Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dapr/ # Core SDK package (single PyPI dist: `pip install
├── flask/ # Flask integration ← see dapr/ext/flask/AGENTS.md (`pip install dapr[flask]`)
├── grpc/ # gRPC App extension ← see dapr/ext/grpc/AGENTS.md (`pip install dapr[grpc]`)
├── langgraph/ # LangGraph checkpointer ← see dapr/ext/langgraph/AGENTS.md (`pip install dapr[langgraph]`)
├── rag/ # Durable RAG ingestion ← see dapr/ext/rag/AGENTS.md (`pip install dapr[rag,...]`)
├── strands/ # Strands agent sessions ← see dapr/ext/strands/AGENTS.md (`pip install dapr[strands]`)
└── workflow/ # Workflow authoring ← see dapr/ext/workflow/AGENTS.md (`pip install dapr[workflow]`)

Expand Down Expand Up @@ -62,6 +63,7 @@ Extensions are bundled into the core `dapr` wheel and exposed as installable ext
| `dapr[flask]` | `dapr.ext.flask` | Flask integration for pub/sub and actors (legacy `flask_dapr` import path is a deprecated shim) | Low |
| `dapr[langgraph]` | `dapr.ext.langgraph` | LangGraph checkpoint persistence to Dapr state store | Moderate |
| `dapr[strands]` | `dapr.ext.strands` | Strands agent session management via Dapr state store | New |
| `dapr[rag]` | `dapr.ext.rag` | Durable RAG ingestion pipeline on Dapr Workflow (S3/Azure sources, pgvector/Pinecone stores) | New |

The previously-separate distributions (`dapr-ext-*`, `flask-dapr`) are no longer published. `dapr/__init__.py` emits a `FutureWarning` if it detects a legacy install at import time; see `RELEASE.md` for the migration recipe.

Expand Down Expand Up @@ -109,6 +111,7 @@ uv run python -m unittest discover -v ./tests/ext/grpc
uv run python -m unittest discover -v ./tests/ext/fastapi
uv run python -m unittest discover -v ./tests/ext/langgraph
uv run python -m unittest discover -v ./tests/ext/strands
uv run python -m unittest discover -v ./tests/ext/rag

# pytest-style suites:
uv run pytest -m "not e2e" ./tests/ext/workflow/durabletask/
Expand Down
438 changes: 438 additions & 0 deletions dapr/ext/rag/AGENTS.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions dapr/ext/rag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# dapr.ext.rag

Durable RAG (retrieval-augmented generation) document ingestion on Dapr Workflow.
`DurableRAGPipeline` discovers documents from S3 or Azure Blob Storage, parses and chunks them,
generates embeddings, and writes them into a versioned pgvector or Pinecone index -- surviving
throttling, process crashes, and pod restarts by resuming from completed work.

```sh
pip install "dapr[rag,rag-s3,rag-pgvector]" # S3 + pgvector
pip install "dapr[rag,rag-azure,rag-pinecone]" # Azure Blob + Pinecone
```

```python
from dapr.ext.rag import DurableRAGPipeline, S3Source, UnstructuredParser, TextSplitter, OpenAIEmbedder, PgVectorStore

pipeline = DurableRAGPipeline(
source=S3Source(bucket='company-docs', prefix='policies/'),
parser=UnstructuredParser(),
splitter=TextSplitter(chunk_size=1000, chunk_overlap=150),
embedder=OpenAIEmbedder(model='text-embedding-3-small'),
vector_store=PgVectorStore(connection_string='...', collection='company-knowledge'),
state_store_name='rag-pipeline-state',
)
instance_id = pipeline.start(version='2026-09', activate_when_complete=True)
```

See [`AGENTS.md`](AGENTS.md) for architecture and internals, [`examples/rag/`](../../../examples/rag)
for a runnable worker/CLI/failure-demo, and [`docs/rag/README.md`](../../../docs/rag/README.md)
for full configuration, authentication, and operational documentation.
201 changes: 201 additions & 0 deletions dapr/ext/rag/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-

"""
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

# Unlike dapr.ext.langgraph/strands (each wrapping exactly one all-or-nothing
# third-party SDK, guarded by a single try/except here in __init__.py), this
# extension bundles several *independent* optional adapters (boto3,
# azure-storage-blob, azure-identity, azure-search-documents, openai, psycopg,
# pinecone, unstructured, langchain-core). Guarding the import here would mean
# the first missing package breaks `import dapr.ext.rag` entirely, even for
# users who only need e.g. pgvector. So each adapter module guards its own
# third-party import instead (see AGENTS.md), and raises
# OptionalDependencyError lazily, from the adapter class's constructor rather
# than at import time -- every import below is therefore unconditional and
# always succeeds.

from dapr.ext.rag.embedding import Embedder, OpenAIEmbedder
from dapr.ext.rag.embedding.azure_openai import AzureOpenAIEmbedder
from dapr.ext.rag.errors import (
ActivationConflictError,
DocumentChangedError,
DocumentParseError,
InvalidEmbeddingRequestError,
InvalidGenerationRequestError,
NonRetryableError,
OptionalDependencyError,
RagError,
RetryableError,
SourceAccessDeniedError,
SourceNotFoundError,
TransientEmbeddingError,
TransientGenerationError,
TransientSourceError,
TransientVectorStoreError,
UnsupportedDocumentError,
VectorStoreError,
VersionValidationError,
)
from dapr.ext.rag.fingerprints import (
compute_chunk_id,
compute_config_hash,
compute_content_hash,
compute_manifest_hash,
compute_pipeline_fingerprint,
)
from dapr.ext.rag.generation import AzureOpenAIChatClient
from dapr.ext.rag.models import (
ActivationRecord,
AnswerResult,
Chunk,
Citation,
CompletionRecord,
Document,
DocumentFailure,
DocumentOutcome,
DocumentOutcomeStatus,
DocumentWorkItem,
EmbeddingBatchResult,
EmbedProgressRecord,
FoundryIQKnowledgeSourceConfig,
ManifestSummary,
PipelineConfig,
PipelineStage,
PipelineStatus,
ProvenanceRecord,
QueryMatch,
SourceChangeEvent,
SourceDocument,
SourceMetadata,
SourceProvider,
UpsertResult,
ValidationResult,
VectorRecord,
)
from dapr.ext.rag.parsing import (
DocumentParser,
UnstructuredParser,
from_langchain_documents,
to_langchain_documents,
)
from dapr.ext.rag.pipeline import DurableRAGPipeline
from dapr.ext.rag.retrieval import ActiveVersionResolver
from dapr.ext.rag.sources import AzureBlobSource, DocumentSource, S3Source
from dapr.ext.rag.splitting import DocumentSplitter, TextSplitter
from dapr.ext.rag.state import PipelineStateStore
from dapr.ext.rag.testing import FailureInjector
from dapr.ext.rag.triggers import (
EventDeduplicator,
StorageEventNotification,
parse_azure_blob_event,
parse_s3_event_notifications,
to_source_change_event,
)
from dapr.ext.rag.vector_stores import (
AzureAISearchVectorStore,
PgVectorStore,
PineconeVectorStore,
VectorIndex,
)

__all__ = [
# Pipeline
'DurableRAGPipeline',
'PipelineConfig',
'PipelineStatus',
'PipelineStage',
'PipelineStateStore',
'FoundryIQKnowledgeSourceConfig',
# Retrieval
'ActiveVersionResolver',
# Generation (query-time; not part of the durable ingestion path)
'AzureOpenAIChatClient',
'AnswerResult',
'Citation',
# Sources
'DocumentSource',
'S3Source',
'AzureBlobSource',
# Parsing
'DocumentParser',
'UnstructuredParser',
'to_langchain_documents',
'from_langchain_documents',
# Splitting
'DocumentSplitter',
'TextSplitter',
# Embedding
'Embedder',
'OpenAIEmbedder',
'AzureOpenAIEmbedder',
'EmbeddingBatchResult',
# Vector stores
'VectorIndex',
'PgVectorStore',
'PineconeVectorStore',
'AzureAISearchVectorStore',
'QueryMatch',
# Models
'SourceDocument',
'SourceMetadata',
'SourceProvider',
'DocumentWorkItem',
'Document',
'Chunk',
'VectorRecord',
'UpsertResult',
'ValidationResult',
'ActivationRecord',
'ProvenanceRecord',
'DocumentOutcome',
'DocumentOutcomeStatus',
'DocumentFailure',
'ManifestSummary',
'CompletionRecord',
'EmbedProgressRecord',
# Fingerprints
'compute_chunk_id',
'compute_config_hash',
'compute_content_hash',
'compute_manifest_hash',
'compute_pipeline_fingerprint',
# Triggers
'SourceChangeEvent',
'StorageEventNotification',
'parse_s3_event_notifications',
'parse_azure_blob_event',
'to_source_change_event',
'EventDeduplicator',
# Testing
'FailureInjector',
# Errors
'RagError',
'OptionalDependencyError',
'RetryableError',
'NonRetryableError',
'TransientSourceError',
'SourceNotFoundError',
'SourceAccessDeniedError',
'DocumentChangedError',
'UnsupportedDocumentError',
'DocumentParseError',
'TransientEmbeddingError',
'InvalidEmbeddingRequestError',
'TransientGenerationError',
'InvalidGenerationRequestError',
'TransientVectorStoreError',
'VectorStoreError',
'VersionValidationError',
'ActivationConflictError',
]
82 changes: 82 additions & 0 deletions dapr/ext/rag/_wire.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-

"""
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

# `dapr.ext.workflow`'s automatic activity input/output coercion
# (`dapr.ext.workflow._model_protocol`) only recognizes Pydantic-v2-shaped
# classes (objects exposing `model_dump` / `model_validate`). This package
# uses plain `@dataclass` types everywhere instead, matching the dataclass
# style used across the rest of the SDK (see `dapr/clients/grpc/_jobs.py`,
# `dapr/ext/workflow/propagation.py`) rather than adding a new hard runtime
# dependency on pydantic. A plain dataclass passed as `call_activity(...,
# input=...)` therefore arrives on the other side undecoded, as a `dict` or
# `SimpleNamespace` -- these two helpers perform that conversion explicitly at
# each activity/workflow boundary.
#
# Every dataclass that crosses an activity boundary must be *flat* (fields are
# JSON primitives, or lists/dicts of them): `dataclasses.asdict` recurses into
# nested dataclass fields on the way out, but `from_wire` does not reconstruct
# them on the way back, so a nested dataclass field would come back as a plain
# dict instead of an instance. Richer nested models (e.g. `SourceDocument`)
# are used only within a single activity's body, never as its input/output.

from __future__ import annotations

import dataclasses
from types import SimpleNamespace
from typing import Any, Mapping, TypeVar

T = TypeVar('T')


def to_wire(value: Any) -> Any:
"""Serializes a flat dataclass instance to a JSON-safe dict.

Args:
value: A dataclass instance, or any already-JSON-safe value.

Returns:
`dataclasses.asdict(value)` if `value` is a dataclass instance,
otherwise `value` unchanged.
"""
if dataclasses.is_dataclass(value) and not isinstance(value, type):
return dataclasses.asdict(value)
return value


def from_wire(raw: Any, cls: type[T]) -> T:
"""Reconstructs a flat dataclass instance from a decoded activity payload.

Args:
raw: The value durabletask handed to the activity/workflow: typically
a `dict`, occasionally a `SimpleNamespace`, or already an
instance of `cls` (e.g. when called directly from a unit test).
cls: The flat dataclass type to reconstruct.

Returns:
An instance of `cls`.

Raises:
TypeError: `raw` is not a dict, SimpleNamespace, or `cls` instance.
"""
if isinstance(raw, cls):
return raw
if isinstance(raw, SimpleNamespace):
raw = vars(raw)
if isinstance(raw, Mapping):
return cls(**raw)
raise TypeError(
f'Cannot interpret {type(raw).__name__!r} as {cls.__name__}; expected a dict, '
'SimpleNamespace, or existing instance.'
)
22 changes: 22 additions & 0 deletions dapr/ext/rag/embedding/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-

"""
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

from dapr.ext.rag.embedding.base import Embedder
from dapr.ext.rag.embedding.openai import OpenAIEmbedder

__all__ = [
'Embedder',
'OpenAIEmbedder',
]
Loading
Loading