agno 2.4.1__py3-none-any.whl → 2.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/db/firestore/firestore.py +58 -65
- agno/db/mysql/async_mysql.py +47 -55
- agno/db/postgres/async_postgres.py +52 -61
- agno/db/sqlite/async_sqlite.py +52 -61
- agno/knowledge/knowledge.py +441 -4
- agno/knowledge/remote_content/__init__.py +4 -0
- agno/knowledge/remote_content/config.py +65 -3
- agno/knowledge/remote_content/remote_content.py +32 -1
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/open_responses.py +46 -0
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/responses.py +146 -0
- agno/os/routers/knowledge/schemas.py +1 -1
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +79 -48
- {agno-2.4.1.dist-info → agno-2.4.2.dist-info}/METADATA +1 -1
- {agno-2.4.1.dist-info → agno-2.4.2.dist-info}/RECORD +22 -19
- {agno-2.4.1.dist-info → agno-2.4.2.dist-info}/WHEEL +0 -0
- {agno-2.4.1.dist-info → agno-2.4.2.dist-info}/licenses/LICENSE +0 -0
- {agno-2.4.1.dist-info → agno-2.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.openai.open_responses import OpenResponses
|
|
6
|
+
from agno.utils.log import log_debug
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class OllamaResponses(OpenResponses):
|
|
11
|
+
"""
|
|
12
|
+
A class for interacting with Ollama models using the OpenAI Responses API.
|
|
13
|
+
|
|
14
|
+
This uses Ollama's OpenAI-compatible `/v1/responses` endpoint, which was added
|
|
15
|
+
in Ollama v0.13.3. It allows using Ollama models with the Responses API format.
|
|
16
|
+
|
|
17
|
+
Note: Ollama's Responses API is stateless - it does not support `previous_response_id`
|
|
18
|
+
or conversation chaining. Each request is independent.
|
|
19
|
+
|
|
20
|
+
Requirements:
|
|
21
|
+
- Ollama v0.13.3 or later
|
|
22
|
+
- For local usage: Ollama server running at http://localhost:11434
|
|
23
|
+
- For Ollama Cloud: Set OLLAMA_API_KEY environment variable
|
|
24
|
+
|
|
25
|
+
For more information, see: https://docs.ollama.com/api/openai-compatibility
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
id (str): The model id. Defaults to "gpt-oss:20b".
|
|
29
|
+
name (str): The model name. Defaults to "OllamaResponses".
|
|
30
|
+
provider (str): The provider name. Defaults to "Ollama".
|
|
31
|
+
host (Optional[str]): The Ollama server host. Defaults to "http://localhost:11434".
|
|
32
|
+
api_key (Optional[str]): The API key for Ollama Cloud. Not required for local usage.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
id: str = "gpt-oss:20b"
|
|
36
|
+
name: str = "OllamaResponses"
|
|
37
|
+
provider: str = "Ollama"
|
|
38
|
+
|
|
39
|
+
# Ollama server host - defaults to local instance
|
|
40
|
+
host: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
# API key for Ollama Cloud (not required for local)
|
|
43
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("OLLAMA_API_KEY"))
|
|
44
|
+
|
|
45
|
+
# Ollama's Responses API is stateless
|
|
46
|
+
store: Optional[bool] = False
|
|
47
|
+
|
|
48
|
+
def _get_client_params(self) -> Dict[str, Any]:
|
|
49
|
+
"""
|
|
50
|
+
Get client parameters for API requests.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dict[str, Any]: Client parameters including base_url and optional api_key.
|
|
54
|
+
"""
|
|
55
|
+
# Determine the base URL
|
|
56
|
+
if self.host:
|
|
57
|
+
base_url = self.host.rstrip("/")
|
|
58
|
+
if not base_url.endswith("/v1"):
|
|
59
|
+
base_url = f"{base_url}/v1"
|
|
60
|
+
elif self.api_key:
|
|
61
|
+
# Ollama Cloud
|
|
62
|
+
base_url = "https://ollama.com/v1"
|
|
63
|
+
log_debug(f"Using Ollama Cloud endpoint: {base_url}")
|
|
64
|
+
else:
|
|
65
|
+
# Local Ollama instance
|
|
66
|
+
base_url = "http://localhost:11434/v1"
|
|
67
|
+
|
|
68
|
+
# Build client params
|
|
69
|
+
base_params: Dict[str, Any] = {
|
|
70
|
+
"base_url": base_url,
|
|
71
|
+
"timeout": self.timeout,
|
|
72
|
+
"max_retries": self.max_retries,
|
|
73
|
+
"default_headers": self.default_headers,
|
|
74
|
+
"default_query": self.default_query,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Add API key if provided (required for Ollama Cloud, ignored for local)
|
|
78
|
+
if self.api_key:
|
|
79
|
+
base_params["api_key"] = self.api_key
|
|
80
|
+
else:
|
|
81
|
+
# OpenAI client requires an api_key, but Ollama ignores it locally
|
|
82
|
+
base_params["api_key"] = "ollama"
|
|
83
|
+
|
|
84
|
+
# Filter out None values
|
|
85
|
+
client_params = {k: v for k, v in base_params.items() if v is not None}
|
|
86
|
+
|
|
87
|
+
# Add additional client params if provided
|
|
88
|
+
if self.client_params:
|
|
89
|
+
client_params.update(self.client_params)
|
|
90
|
+
|
|
91
|
+
return client_params
|
|
92
|
+
|
|
93
|
+
def _using_reasoning_model(self) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Ollama doesn't have native reasoning models like OpenAI's o-series.
|
|
96
|
+
|
|
97
|
+
Some models may support thinking/reasoning through their architecture
|
|
98
|
+
(like DeepSeek-R1), but they don't use OpenAI's reasoning API format.
|
|
99
|
+
"""
|
|
100
|
+
return False
|
agno/models/openai/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from agno.models.openai.chat import OpenAIChat
|
|
2
2
|
from agno.models.openai.like import OpenAILike
|
|
3
|
+
from agno.models.openai.open_responses import OpenResponses
|
|
3
4
|
from agno.models.openai.responses import OpenAIResponses
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
6
7
|
"OpenAIChat",
|
|
7
8
|
"OpenAILike",
|
|
8
9
|
"OpenAIResponses",
|
|
10
|
+
"OpenResponses",
|
|
9
11
|
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from agno.models.openai.responses import OpenAIResponses
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class OpenResponses(OpenAIResponses):
|
|
9
|
+
"""
|
|
10
|
+
A base class for interacting with any provider using the Open Responses API specification.
|
|
11
|
+
|
|
12
|
+
Open Responses is an open-source specification for building multi-provider, interoperable
|
|
13
|
+
LLM interfaces based on the OpenAI Responses API. This class provides a foundation for
|
|
14
|
+
providers that implement the spec (e.g., Ollama, OpenRouter).
|
|
15
|
+
|
|
16
|
+
For more information, see: https://openresponses.org
|
|
17
|
+
|
|
18
|
+
Key differences from OpenAIResponses:
|
|
19
|
+
- Configurable base_url for pointing to different API endpoints
|
|
20
|
+
- Stateless by default (no previous_response_id chaining)
|
|
21
|
+
- Flexible api_key handling for providers that don't require authentication
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
id (str): The model id. Defaults to "not-provided".
|
|
25
|
+
name (str): The model name. Defaults to "OpenResponses".
|
|
26
|
+
api_key (Optional[str]): The API key. Defaults to "not-provided".
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
id: str = "not-provided"
|
|
30
|
+
name: str = "OpenResponses"
|
|
31
|
+
provider: str = "OpenResponses"
|
|
32
|
+
api_key: Optional[str] = "not-provided"
|
|
33
|
+
|
|
34
|
+
# Disable stateful features by default for compatible providers
|
|
35
|
+
# Most OpenAI-compatible providers don't support previous_response_id chaining
|
|
36
|
+
store: Optional[bool] = False
|
|
37
|
+
|
|
38
|
+
def _using_reasoning_model(self) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Override to disable reasoning model detection for compatible providers.
|
|
41
|
+
|
|
42
|
+
Most compatible providers don't support OpenAI's reasoning models,
|
|
43
|
+
so we disable the special handling by default. Subclasses can override
|
|
44
|
+
this if they support specific reasoning models.
|
|
45
|
+
"""
|
|
46
|
+
return False
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from agno.exceptions import ModelAuthenticationError
|
|
8
|
+
from agno.models.openai.open_responses import OpenResponses
|
|
9
|
+
from agno.models.message import Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class OpenRouterResponses(OpenResponses):
|
|
14
|
+
"""
|
|
15
|
+
A class for interacting with OpenRouter models using the OpenAI Responses API.
|
|
16
|
+
|
|
17
|
+
OpenRouter's Responses API (currently in beta) provides OpenAI-compatible access
|
|
18
|
+
to multiple AI models through a unified interface. It supports tools, reasoning,
|
|
19
|
+
streaming, and plugins.
|
|
20
|
+
|
|
21
|
+
Note: OpenRouter's Responses API is stateless - each request is independent and
|
|
22
|
+
no server-side state is persisted.
|
|
23
|
+
|
|
24
|
+
For more information, see: https://openrouter.ai/docs/api/reference/responses/overview
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
id (str): The model id. Defaults to "openai/gpt-oss-20b".
|
|
28
|
+
name (str): The model name. Defaults to "OpenRouterResponses".
|
|
29
|
+
provider (str): The provider name. Defaults to "OpenRouter".
|
|
30
|
+
api_key (Optional[str]): The API key. Uses OPENROUTER_API_KEY env var if not set.
|
|
31
|
+
base_url (str): The base URL. Defaults to "https://openrouter.ai/api/v1".
|
|
32
|
+
models (Optional[List[str]]): List of fallback model IDs to use if the primary model
|
|
33
|
+
fails due to rate limits, timeouts, or unavailability. OpenRouter will automatically
|
|
34
|
+
try these models in order. Example: ["anthropic/claude-sonnet-4", "deepseek/deepseek-r1"]
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
```python
|
|
38
|
+
from agno.agent import Agent
|
|
39
|
+
from agno.models.openrouter import OpenRouterResponses
|
|
40
|
+
|
|
41
|
+
agent = Agent(
|
|
42
|
+
model=OpenRouterResponses(id="anthropic/claude-sonnet-4"),
|
|
43
|
+
markdown=True,
|
|
44
|
+
)
|
|
45
|
+
agent.print_response("Write a haiku about coding")
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
id: str = "openai/gpt-oss-20b"
|
|
50
|
+
name: str = "OpenRouterResponses"
|
|
51
|
+
provider: str = "OpenRouter"
|
|
52
|
+
|
|
53
|
+
api_key: Optional[str] = None
|
|
54
|
+
base_url: str = "https://openrouter.ai/api/v1"
|
|
55
|
+
|
|
56
|
+
# Dynamic model routing - fallback models if primary fails
|
|
57
|
+
# https://openrouter.ai/docs/features/model-routing
|
|
58
|
+
models: Optional[List[str]] = None
|
|
59
|
+
|
|
60
|
+
# OpenRouter's Responses API is stateless
|
|
61
|
+
store: Optional[bool] = False
|
|
62
|
+
|
|
63
|
+
def _get_client_params(self) -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Returns client parameters for API requests, checking for OPENROUTER_API_KEY.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict[str, Any]: A dictionary of client parameters for API requests.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ModelAuthenticationError: If OPENROUTER_API_KEY is not set.
|
|
72
|
+
"""
|
|
73
|
+
# Fetch API key from env if not already set
|
|
74
|
+
if not self.api_key:
|
|
75
|
+
self.api_key = getenv("OPENROUTER_API_KEY")
|
|
76
|
+
if not self.api_key:
|
|
77
|
+
raise ModelAuthenticationError(
|
|
78
|
+
message="OPENROUTER_API_KEY not set. Please set the OPENROUTER_API_KEY environment variable.",
|
|
79
|
+
model_name=self.name,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Build client params
|
|
83
|
+
base_params: Dict[str, Any] = {
|
|
84
|
+
"api_key": self.api_key,
|
|
85
|
+
"base_url": self.base_url,
|
|
86
|
+
"organization": self.organization,
|
|
87
|
+
"timeout": self.timeout,
|
|
88
|
+
"max_retries": self.max_retries,
|
|
89
|
+
"default_headers": self.default_headers,
|
|
90
|
+
"default_query": self.default_query,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Filter out None values
|
|
94
|
+
client_params = {k: v for k, v in base_params.items() if v is not None}
|
|
95
|
+
|
|
96
|
+
# Add additional client params if provided
|
|
97
|
+
if self.client_params:
|
|
98
|
+
client_params.update(self.client_params)
|
|
99
|
+
|
|
100
|
+
return client_params
|
|
101
|
+
|
|
102
|
+
def get_request_params(
|
|
103
|
+
self,
|
|
104
|
+
messages: Optional[List[Message]] = None,
|
|
105
|
+
response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
|
|
106
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
107
|
+
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Returns keyword arguments for API requests, including fallback models configuration.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dict[str, Any]: A dictionary of keyword arguments for API requests.
|
|
114
|
+
"""
|
|
115
|
+
# Get base request params from parent class
|
|
116
|
+
request_params = super().get_request_params(
|
|
117
|
+
messages=messages,
|
|
118
|
+
response_format=response_format,
|
|
119
|
+
tools=tools,
|
|
120
|
+
tool_choice=tool_choice,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Add fallback models to extra_body if specified
|
|
124
|
+
if self.models:
|
|
125
|
+
# Get existing extra_body or create new dict
|
|
126
|
+
extra_body = request_params.get("extra_body") or {}
|
|
127
|
+
|
|
128
|
+
# Merge fallback models into extra_body
|
|
129
|
+
extra_body["models"] = self.models
|
|
130
|
+
|
|
131
|
+
# Update request params
|
|
132
|
+
request_params["extra_body"] = extra_body
|
|
133
|
+
|
|
134
|
+
return request_params
|
|
135
|
+
|
|
136
|
+
def _using_reasoning_model(self) -> bool:
|
|
137
|
+
"""
|
|
138
|
+
Check if the model is a reasoning model that requires special handling.
|
|
139
|
+
|
|
140
|
+
OpenRouter hosts various reasoning models, but they may not all use
|
|
141
|
+
OpenAI's reasoning API format. We check for known reasoning model patterns.
|
|
142
|
+
"""
|
|
143
|
+
# Check for OpenAI reasoning models hosted on OpenRouter
|
|
144
|
+
if self.id.startswith("openai/o3") or self.id.startswith("openai/o4"):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
@@ -175,7 +175,7 @@ class RemoteContentSourceSchema(BaseModel):
|
|
|
175
175
|
|
|
176
176
|
id: str = Field(..., description="Unique identifier for the content source")
|
|
177
177
|
name: str = Field(..., description="Display name for the content source")
|
|
178
|
-
type: str = Field(..., description="Type of content source (s3, gcs, sharepoint, github)")
|
|
178
|
+
type: str = Field(..., description="Type of content source (s3, gcs, sharepoint, github, azureblob)")
|
|
179
179
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Custom metadata for the content source")
|
|
180
180
|
|
|
181
181
|
|
|
@@ -109,7 +109,7 @@ class LightRag(VectorDb):
|
|
|
109
109
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
110
110
|
response = await client.post(
|
|
111
111
|
f"{self.server_url}/query",
|
|
112
|
-
json={"query": query, "mode": "hybrid"},
|
|
112
|
+
json={"query": query, "mode": "hybrid", "include_references": True},
|
|
113
113
|
headers=self._get_headers(),
|
|
114
114
|
)
|
|
115
115
|
|
|
@@ -322,7 +322,7 @@ class LightRag(VectorDb):
|
|
|
322
322
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
323
323
|
response = await client.post(
|
|
324
324
|
f"{self.server_url}/query",
|
|
325
|
-
json={"query": query, "mode": "hybrid"},
|
|
325
|
+
json={"query": query, "mode": "hybrid", "include_references": True},
|
|
326
326
|
headers=self._get_headers(),
|
|
327
327
|
)
|
|
328
328
|
|
|
@@ -349,10 +349,11 @@ class LightRag(VectorDb):
|
|
|
349
349
|
# LightRAG server returns a dict with 'response' key, but we expect a list of documents
|
|
350
350
|
# Convert the response to the expected format
|
|
351
351
|
if isinstance(result, dict) and "response" in result:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
352
|
+
meta_data = {"source": "lightrag", "query": query, "mode": mode}
|
|
353
|
+
# Preserve references from LightRAG response for document citations
|
|
354
|
+
if "references" in result:
|
|
355
|
+
meta_data["references"] = result["references"]
|
|
356
|
+
return [Document(content=result["response"], meta_data=meta_data)]
|
|
356
357
|
elif isinstance(result, list):
|
|
357
358
|
# Convert list items to Document objects
|
|
358
359
|
documents = []
|
agno/vectordb/milvus/milvus.py
CHANGED
|
@@ -241,7 +241,7 @@ class Milvus(VectorDb):
|
|
|
241
241
|
"id": doc_id,
|
|
242
242
|
"text": cleaned_content,
|
|
243
243
|
"name": document.name,
|
|
244
|
-
"content_id": document.content_id,
|
|
244
|
+
"content_id": document.content_id or "",
|
|
245
245
|
"meta_data": meta_data_str,
|
|
246
246
|
"content": cleaned_content,
|
|
247
247
|
"usage": usage_str,
|
|
@@ -334,6 +334,7 @@ class Milvus(VectorDb):
|
|
|
334
334
|
scroll_result = self.client.query(
|
|
335
335
|
collection_name=self.collection,
|
|
336
336
|
filter=expr,
|
|
337
|
+
output_fields=["id"],
|
|
337
338
|
limit=1,
|
|
338
339
|
)
|
|
339
340
|
return len(scroll_result) > 0 and len(scroll_result[0]) > 0
|
|
@@ -363,6 +364,7 @@ class Milvus(VectorDb):
|
|
|
363
364
|
scroll_result = self.client.query(
|
|
364
365
|
collection_name=self.collection,
|
|
365
366
|
filter=expr,
|
|
367
|
+
output_fields=["id"],
|
|
366
368
|
limit=1,
|
|
367
369
|
)
|
|
368
370
|
return len(scroll_result) > 0 and len(scroll_result[0]) > 0
|
|
@@ -429,7 +431,7 @@ class Milvus(VectorDb):
|
|
|
429
431
|
"id": doc_id,
|
|
430
432
|
"vector": document.embedding,
|
|
431
433
|
"name": document.name,
|
|
432
|
-
"content_id": document.content_id,
|
|
434
|
+
"content_id": document.content_id or "",
|
|
433
435
|
"meta_data": meta_data,
|
|
434
436
|
"content": cleaned_content,
|
|
435
437
|
"usage": document.usage,
|
|
@@ -512,7 +514,7 @@ class Milvus(VectorDb):
|
|
|
512
514
|
"id": doc_id,
|
|
513
515
|
"vector": document.embedding,
|
|
514
516
|
"name": document.name,
|
|
515
|
-
"content_id": document.content_id,
|
|
517
|
+
"content_id": document.content_id or "",
|
|
516
518
|
"meta_data": meta_data,
|
|
517
519
|
"content": cleaned_content,
|
|
518
520
|
"usage": document.usage,
|
|
@@ -547,30 +549,41 @@ class Milvus(VectorDb):
|
|
|
547
549
|
filters (Optional[Dict[str, Any]]): Filters to apply while upserting
|
|
548
550
|
"""
|
|
549
551
|
log_debug(f"Upserting {len(documents)} documents")
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
552
|
+
|
|
553
|
+
if self.search_type == SearchType.hybrid:
|
|
554
|
+
for document in documents:
|
|
555
|
+
document.embed(embedder=self.embedder)
|
|
556
|
+
data = self._prepare_document_data(content_hash=content_hash, document=document, include_vectors=True)
|
|
557
|
+
self.client.upsert(
|
|
558
|
+
collection_name=self.collection,
|
|
559
|
+
data=data,
|
|
560
|
+
)
|
|
561
|
+
log_debug(f"Upserted hybrid document: {document.name} ({document.meta_data})")
|
|
562
|
+
else:
|
|
563
|
+
for document in documents:
|
|
564
|
+
document.embed(embedder=self.embedder)
|
|
565
|
+
cleaned_content = document.content.replace("\x00", "\ufffd")
|
|
566
|
+
doc_id = md5(cleaned_content.encode()).hexdigest()
|
|
567
|
+
|
|
568
|
+
meta_data = document.meta_data or {}
|
|
569
|
+
if filters:
|
|
570
|
+
meta_data.update(filters)
|
|
571
|
+
|
|
572
|
+
data = {
|
|
573
|
+
"id": doc_id,
|
|
574
|
+
"vector": document.embedding,
|
|
575
|
+
"name": document.name,
|
|
576
|
+
"content_id": document.content_id or "",
|
|
577
|
+
"meta_data": meta_data, # type: ignore[dict-item]
|
|
578
|
+
"content": cleaned_content,
|
|
579
|
+
"usage": document.usage, # type: ignore[dict-item]
|
|
580
|
+
"content_hash": content_hash,
|
|
581
|
+
}
|
|
582
|
+
self.client.upsert(
|
|
583
|
+
collection_name=self.collection,
|
|
584
|
+
data=data,
|
|
585
|
+
)
|
|
586
|
+
log_debug(f"Upserted document: {document.name} ({document.meta_data})")
|
|
574
587
|
|
|
575
588
|
async def async_upsert(
|
|
576
589
|
self, content_hash: str, documents: List[Document], filters: Optional[Dict[str, Any]] = None
|
|
@@ -616,28 +629,46 @@ class Milvus(VectorDb):
|
|
|
616
629
|
embed_tasks = [document.async_embed(embedder=self.embedder) for document in documents]
|
|
617
630
|
await asyncio.gather(*embed_tasks, return_exceptions=True)
|
|
618
631
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
"
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
632
|
+
if self.search_type == SearchType.hybrid:
|
|
633
|
+
|
|
634
|
+
async def process_hybrid_document(document):
|
|
635
|
+
data = self._prepare_document_data(content_hash=content_hash, document=document, include_vectors=True)
|
|
636
|
+
await self.async_client.upsert(
|
|
637
|
+
collection_name=self.collection,
|
|
638
|
+
data=data,
|
|
639
|
+
)
|
|
640
|
+
log_debug(f"Upserted hybrid document asynchronously: {document.name} ({document.meta_data})")
|
|
641
|
+
return data
|
|
642
|
+
|
|
643
|
+
await asyncio.gather(*[process_hybrid_document(doc) for doc in documents])
|
|
644
|
+
else:
|
|
645
|
+
|
|
646
|
+
async def process_document(document):
|
|
647
|
+
cleaned_content = document.content.replace("\x00", "\ufffd")
|
|
648
|
+
doc_id = md5(cleaned_content.encode()).hexdigest()
|
|
649
|
+
|
|
650
|
+
meta_data = document.meta_data or {}
|
|
651
|
+
if filters:
|
|
652
|
+
meta_data.update(filters)
|
|
653
|
+
|
|
654
|
+
data = {
|
|
655
|
+
"id": doc_id,
|
|
656
|
+
"vector": document.embedding,
|
|
657
|
+
"name": document.name,
|
|
658
|
+
"content_id": document.content_id or "",
|
|
659
|
+
"meta_data": meta_data, # type: ignore[dict-item]
|
|
660
|
+
"content": cleaned_content,
|
|
661
|
+
"usage": document.usage, # type: ignore[dict-item]
|
|
662
|
+
"content_hash": content_hash,
|
|
663
|
+
}
|
|
664
|
+
await self.async_client.upsert(
|
|
665
|
+
collection_name=self.collection,
|
|
666
|
+
data=data,
|
|
667
|
+
)
|
|
668
|
+
log_debug(f"Upserted document asynchronously: {document.name} ({document.meta_data})")
|
|
669
|
+
return data
|
|
638
670
|
|
|
639
|
-
|
|
640
|
-
await asyncio.gather(*[process_document(doc) for doc in documents])
|
|
671
|
+
await asyncio.gather(*[process_document(doc) for doc in documents])
|
|
641
672
|
|
|
642
673
|
log_debug(f"Upserted {len(documents)} documents asynchronously in parallel")
|
|
643
674
|
|