agno 2.3.10__py3-none-any.whl → 2.3.11__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/sqlite/sqlite.py CHANGED
@@ -31,7 +31,7 @@ from agno.utils.log import log_debug, log_error, log_info, log_warning
31
31
  from agno.utils.string import generate_id
32
32
 
33
33
  try:
34
- from sqlalchemy import Column, MetaData, String, Table, func, select, text, update
34
+ from sqlalchemy import Column, MetaData, String, Table, func, select, text
35
35
  from sqlalchemy.dialects import sqlite
36
36
  from sqlalchemy.engine import Engine, create_engine
37
37
  from sqlalchemy.orm import scoped_session, sessionmaker
@@ -2145,85 +2145,113 @@ class SqliteDb(BaseDb):
2145
2145
  # Fallback if spans table doesn't exist
2146
2146
  return select(table, literal(0).label("total_spans"), literal(0).label("error_count"))
2147
2147
 
2148
- def create_trace(self, trace: "Trace") -> None:
2149
- """Create a single trace record in the database.
2148
+ def _get_trace_component_level_expr(self, workflow_id_col, team_id_col, agent_id_col, name_col):
2149
+ """Build a SQL CASE expression that returns the component level for a trace.
2150
+
2151
+ Component levels (higher = more important):
2152
+ - 3: Workflow root (.run or .arun with workflow_id)
2153
+ - 2: Team root (.run or .arun with team_id)
2154
+ - 1: Agent root (.run or .arun with agent_id)
2155
+ - 0: Child span (not a root)
2156
+
2157
+ Args:
2158
+ workflow_id_col: SQL column/expression for workflow_id
2159
+ team_id_col: SQL column/expression for team_id
2160
+ agent_id_col: SQL column/expression for agent_id
2161
+ name_col: SQL column/expression for name
2162
+
2163
+ Returns:
2164
+ SQLAlchemy CASE expression returning the component level as an integer.
2165
+ """
2166
+ from sqlalchemy import and_, case, or_
2167
+
2168
+ is_root_name = or_(name_col.contains(".run"), name_col.contains(".arun"))
2169
+
2170
+ return case(
2171
+ # Workflow root (level 3)
2172
+ (and_(workflow_id_col.isnot(None), is_root_name), 3),
2173
+ # Team root (level 2)
2174
+ (and_(team_id_col.isnot(None), is_root_name), 2),
2175
+ # Agent root (level 1)
2176
+ (and_(agent_id_col.isnot(None), is_root_name), 1),
2177
+ # Child span or unknown (level 0)
2178
+ else_=0,
2179
+ )
2180
+
2181
+ def upsert_trace(self, trace: "Trace") -> None:
2182
+ """Create or update a single trace record in the database.
2183
+
2184
+ Uses INSERT ... ON CONFLICT DO UPDATE (upsert) to handle concurrent inserts
2185
+ atomically and avoid race conditions.
2150
2186
 
2151
2187
  Args:
2152
2188
  trace: The Trace object to store (one per trace_id).
2153
2189
  """
2190
+ from sqlalchemy import case
2191
+
2154
2192
  try:
2155
2193
  table = self._get_table(table_type="traces", create_table_if_not_found=True)
2156
2194
  if table is None:
2157
2195
  return
2158
2196
 
2159
- with self.Session() as sess, sess.begin():
2160
- # Check if trace exists
2161
- existing = sess.execute(table.select().where(table.c.trace_id == trace.trace_id)).fetchone()
2162
-
2163
- if existing:
2164
- # workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
2165
-
2166
- def get_component_level(workflow_id, team_id, agent_id, name):
2167
- # Check if name indicates a root span
2168
- is_root_name = ".run" in name or ".arun" in name
2169
-
2170
- if not is_root_name:
2171
- return 0 # Child span (not a root)
2172
- elif workflow_id:
2173
- return 3 # Workflow root
2174
- elif team_id:
2175
- return 2 # Team root
2176
- elif agent_id:
2177
- return 1 # Agent root
2178
- else:
2179
- return 0 # Unknown
2180
-
2181
- existing_level = get_component_level(
2182
- existing.workflow_id, existing.team_id, existing.agent_id, existing.name
2183
- )
2184
- new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
2185
-
2186
- # Only update name if new trace is from a higher or equal level
2187
- should_update_name = new_level > existing_level
2197
+ trace_dict = trace.to_dict()
2198
+ trace_dict.pop("total_spans", None)
2199
+ trace_dict.pop("error_count", None)
2188
2200
 
2189
- # Parse existing start_time to calculate correct duration
2190
- existing_start_time_str = existing.start_time
2191
- if isinstance(existing_start_time_str, str):
2192
- existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
2193
- else:
2194
- existing_start_time = trace.start_time
2195
-
2196
- recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
2197
-
2198
- update_values = {
2199
- "end_time": trace.end_time.isoformat(),
2200
- "duration_ms": recalculated_duration_ms,
2201
- "status": trace.status,
2202
- "name": trace.name if should_update_name else existing.name,
2203
- }
2201
+ with self.Session() as sess, sess.begin():
2202
+ # Use upsert to handle concurrent inserts atomically
2203
+ # On conflict, update fields while preserving existing non-null context values
2204
+ # and keeping the earliest start_time
2205
+ insert_stmt = sqlite.insert(table).values(trace_dict)
2206
+
2207
+ # Build component level expressions for comparing trace priority
2208
+ new_level = self._get_trace_component_level_expr(
2209
+ insert_stmt.excluded.workflow_id,
2210
+ insert_stmt.excluded.team_id,
2211
+ insert_stmt.excluded.agent_id,
2212
+ insert_stmt.excluded.name,
2213
+ )
2214
+ existing_level = self._get_trace_component_level_expr(
2215
+ table.c.workflow_id,
2216
+ table.c.team_id,
2217
+ table.c.agent_id,
2218
+ table.c.name,
2219
+ )
2204
2220
 
2205
- # Update context fields ONLY if new value is not None (preserve non-null values)
2206
- if trace.run_id is not None:
2207
- update_values["run_id"] = trace.run_id
2208
- if trace.session_id is not None:
2209
- update_values["session_id"] = trace.session_id
2210
- if trace.user_id is not None:
2211
- update_values["user_id"] = trace.user_id
2212
- if trace.agent_id is not None:
2213
- update_values["agent_id"] = trace.agent_id
2214
- if trace.team_id is not None:
2215
- update_values["team_id"] = trace.team_id
2216
- if trace.workflow_id is not None:
2217
- update_values["workflow_id"] = trace.workflow_id
2218
-
2219
- stmt = update(table).where(table.c.trace_id == trace.trace_id).values(**update_values)
2220
- sess.execute(stmt)
2221
- else:
2222
- trace_dict = trace.to_dict()
2223
- trace_dict.pop("total_spans", None)
2224
- trace_dict.pop("error_count", None)
2225
- stmt = sqlite.insert(table).values(trace_dict)
2226
- sess.execute(stmt)
2221
+ # Build the ON CONFLICT DO UPDATE clause
2222
+ # Use MIN for start_time, MAX for end_time to capture full trace duration
2223
+ # SQLite stores timestamps as ISO strings, so string comparison works for ISO format
2224
+ # Duration is calculated as: (MAX(end_time) - MIN(start_time)) in milliseconds
2225
+ # SQLite doesn't have epoch extraction, so we calculate duration using julianday
2226
+ upsert_stmt = insert_stmt.on_conflict_do_update(
2227
+ index_elements=["trace_id"],
2228
+ set_={
2229
+ "end_time": func.max(table.c.end_time, insert_stmt.excluded.end_time),
2230
+ "start_time": func.min(table.c.start_time, insert_stmt.excluded.start_time),
2231
+ # Calculate duration in milliseconds using julianday (SQLite-specific)
2232
+ # julianday returns days, so multiply by 86400000 to get milliseconds
2233
+ "duration_ms": (
2234
+ func.julianday(func.max(table.c.end_time, insert_stmt.excluded.end_time))
2235
+ - func.julianday(func.min(table.c.start_time, insert_stmt.excluded.start_time))
2236
+ )
2237
+ * 86400000,
2238
+ "status": insert_stmt.excluded.status,
2239
+ # Update name only if new trace is from a higher-level component
2240
+ # Priority: workflow (3) > team (2) > agent (1) > child spans (0)
2241
+ "name": case(
2242
+ (new_level > existing_level, insert_stmt.excluded.name),
2243
+ else_=table.c.name,
2244
+ ),
2245
+ # Preserve existing non-null context values using COALESCE
2246
+ "run_id": func.coalesce(insert_stmt.excluded.run_id, table.c.run_id),
2247
+ "session_id": func.coalesce(insert_stmt.excluded.session_id, table.c.session_id),
2248
+ "user_id": func.coalesce(insert_stmt.excluded.user_id, table.c.user_id),
2249
+ "agent_id": func.coalesce(insert_stmt.excluded.agent_id, table.c.agent_id),
2250
+ "team_id": func.coalesce(insert_stmt.excluded.team_id, table.c.team_id),
2251
+ "workflow_id": func.coalesce(insert_stmt.excluded.workflow_id, table.c.workflow_id),
2252
+ },
2253
+ )
2254
+ sess.execute(upsert_stmt)
2227
2255
 
2228
2256
  except Exception as e:
2229
2257
  log_error(f"Error creating trace: {e}")
@@ -1390,8 +1390,8 @@ class SurrealDb(BaseDb):
1390
1390
  return deserialize_eval_run_record(raw)
1391
1391
 
1392
1392
  # --- Traces ---
1393
- def create_trace(self, trace: "Trace") -> None:
1394
- """Create a single trace record in the database.
1393
+ def upsert_trace(self, trace: "Trace") -> None:
1394
+ """Create or update a single trace record in the database.
1395
1395
 
1396
1396
  Args:
1397
1397
  trace: The Trace object to store (one per trace_id).
@@ -53,5 +53,8 @@ class FixedSizeChunking(ChunkingStrategy):
53
53
  )
54
54
  )
55
55
  chunk_number += 1
56
- start = end - self.overlap
56
+ # Ensure start always advances by at least 1 to prevent infinite loops
57
+ # when overlap is large relative to chunk_size
58
+ new_start = max(start + 1, end - self.overlap)
59
+ start = new_start
57
60
  return chunked_documents
@@ -1960,8 +1960,8 @@ class Knowledge:
1960
1960
  content_row.updated_at = int(time.time())
1961
1961
  self.contents_db.upsert_knowledge_content(knowledge_row=content_row)
1962
1962
 
1963
- if self.vector_db and content.metadata:
1964
- self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata)
1963
+ if self.vector_db:
1964
+ self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata or {})
1965
1965
 
1966
1966
  return content_row.to_dict()
1967
1967
 
@@ -2006,8 +2006,8 @@ class Knowledge:
2006
2006
  else:
2007
2007
  self.contents_db.upsert_knowledge_content(knowledge_row=content_row)
2008
2008
 
2009
- if self.vector_db and content.metadata:
2010
- self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata)
2009
+ if self.vector_db:
2010
+ self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata or {})
2011
2011
 
2012
2012
  return content_row.to_dict()
2013
2013
 
@@ -2783,6 +2783,24 @@ class Knowledge:
2783
2783
  """Get all currently loaded readers (only returns readers that have been used)."""
2784
2784
  if self.readers is None:
2785
2785
  self.readers = {}
2786
+ elif not isinstance(self.readers, dict):
2787
+ # Defensive check: if readers is not a dict (e.g., was set to a list), convert it
2788
+ if isinstance(self.readers, list):
2789
+ readers_dict: Dict[str, Reader] = {}
2790
+ for reader in self.readers:
2791
+ if isinstance(reader, Reader):
2792
+ reader_key = self._generate_reader_key(reader)
2793
+ # Handle potential duplicate keys by appending index if needed
2794
+ original_key = reader_key
2795
+ counter = 1
2796
+ while reader_key in readers_dict:
2797
+ reader_key = f"{original_key}_{counter}"
2798
+ counter += 1
2799
+ readers_dict[reader_key] = reader
2800
+ self.readers = readers_dict
2801
+ else:
2802
+ # For any other unexpected type, reset to empty dict
2803
+ self.readers = {}
2786
2804
 
2787
2805
  return self.readers
2788
2806
 
agno/knowledge/utils.py CHANGED
@@ -1,5 +1,6 @@
1
- from typing import Dict, List
1
+ from typing import Any, Dict, List, Optional
2
2
 
3
+ from agno.knowledge.reader.base import Reader
3
4
  from agno.knowledge.reader.reader_factory import ReaderFactory
4
5
  from agno.knowledge.types import ContentType
5
6
  from agno.utils.log import log_debug
@@ -75,8 +76,33 @@ def get_reader_info(reader_key: str) -> Dict:
75
76
  raise ValueError(f"Unknown reader: {reader_key}. Error: {str(e)}")
76
77
 
77
78
 
78
- def get_all_readers_info() -> List[Dict]:
79
- """Get information about all available readers."""
79
+ def get_reader_info_from_instance(reader: Reader, reader_id: str) -> Dict:
80
+ """Get information about a reader instance."""
81
+ try:
82
+ reader_class = reader.__class__
83
+ supported_strategies = reader_class.get_supported_chunking_strategies()
84
+ supported_content_types = reader_class.get_supported_content_types()
85
+
86
+ return {
87
+ "id": reader_id,
88
+ "name": getattr(reader, "name", reader_class.__name__),
89
+ "description": getattr(reader, "description", f"Custom {reader_class.__name__}"),
90
+ "chunking_strategies": [strategy.value for strategy in supported_strategies],
91
+ "content_types": [ct.value for ct in supported_content_types],
92
+ }
93
+ except Exception as e:
94
+ raise ValueError(f"Failed to get info for reader '{reader_id}': {str(e)}")
95
+
96
+
97
+ def get_all_readers_info(knowledge_instance: Optional[Any] = None) -> List[Dict]:
98
+ """Get information about all available readers, including custom readers from a Knowledge instance.
99
+
100
+ Args:
101
+ knowledge_instance: Optional Knowledge instance to include custom readers from.
102
+
103
+ Returns:
104
+ List of reader info dictionaries.
105
+ """
80
106
  readers_info = []
81
107
  keys = ReaderFactory.get_all_reader_keys()
82
108
  for key in keys:
@@ -88,18 +114,35 @@ def get_all_readers_info() -> List[Dict]:
88
114
  # Log the error but don't fail the entire request
89
115
  log_debug(f"Skipping reader '{key}': {e}")
90
116
  continue
117
+
118
+ # Add custom readers from knowledge instance if provided
119
+ if knowledge_instance is not None:
120
+ custom_readers = knowledge_instance.get_readers()
121
+ if isinstance(custom_readers, dict):
122
+ for reader_id, reader in custom_readers.items():
123
+ try:
124
+ reader_info = get_reader_info_from_instance(reader, reader_id)
125
+ # Only add if not already present (custom readers take precedence)
126
+ if not any(r["id"] == reader_id for r in readers_info):
127
+ readers_info.append(reader_info)
128
+ except ValueError as e:
129
+ log_debug(f"Skipping custom reader '{reader_id}': {e}")
130
+ continue
131
+
91
132
  return readers_info
92
133
 
93
134
 
94
- def get_content_types_to_readers_mapping() -> Dict[str, List[str]]:
135
+ def get_content_types_to_readers_mapping(knowledge_instance: Optional[Any] = None) -> Dict[str, List[str]]:
95
136
  """Get mapping of content types to list of reader IDs that support them.
96
137
 
138
+ Args:
139
+ knowledge_instance: Optional Knowledge instance to include custom readers from.
140
+
97
141
  Returns:
98
142
  Dictionary mapping content type strings (ContentType enum values) to list of reader IDs.
99
143
  """
100
144
  content_type_mapping: Dict[str, List[str]] = {}
101
- readers_info = get_all_readers_info()
102
-
145
+ readers_info = get_all_readers_info(knowledge_instance)
103
146
  for reader_info in readers_info:
104
147
  reader_id = reader_info["id"]
105
148
  content_types = reader_info.get("content_types", [])
@@ -107,7 +150,9 @@ def get_content_types_to_readers_mapping() -> Dict[str, List[str]]:
107
150
  for content_type in content_types:
108
151
  if content_type not in content_type_mapping:
109
152
  content_type_mapping[content_type] = []
110
- content_type_mapping[content_type].append(reader_id)
153
+ # Avoid duplicates
154
+ if reader_id not in content_type_mapping[content_type]:
155
+ content_type_mapping[content_type].append(reader_id)
111
156
 
112
157
  return content_type_mapping
113
158
 
@@ -820,6 +820,16 @@ class OpenAIChat(Model):
820
820
  if response.usage is not None:
821
821
  model_response.response_usage = self._get_metrics(response.usage)
822
822
 
823
+ if model_response.provider_data is None:
824
+ model_response.provider_data = {}
825
+
826
+ if response.id:
827
+ model_response.provider_data["id"] = response.id
828
+ if response.system_fingerprint:
829
+ model_response.provider_data["system_fingerprint"] = response.system_fingerprint
830
+ if response.model_extra:
831
+ model_response.provider_data["model_extra"] = response.model_extra
832
+
823
833
  return model_response
824
834
 
825
835
  def _parse_provider_response_delta(self, response_delta: ChatCompletionChunk) -> ModelResponse:
@@ -842,6 +852,17 @@ class OpenAIChat(Model):
842
852
  if choice_delta.content is not None:
843
853
  model_response.content = choice_delta.content
844
854
 
855
+ # We only want to handle these if content is present
856
+ if model_response.provider_data is None:
857
+ model_response.provider_data = {}
858
+
859
+ if response_delta.id:
860
+ model_response.provider_data["id"] = response_delta.id
861
+ if response_delta.system_fingerprint:
862
+ model_response.provider_data["system_fingerprint"] = response_delta.system_fingerprint
863
+ if response_delta.model_extra:
864
+ model_response.provider_data["model_extra"] = response_delta.model_extra
865
+
845
866
  # Add tool calls
846
867
  if choice_delta.tool_calls is not None:
847
868
  model_response.tool_calls = choice_delta.tool_calls # type: ignore
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  import math
4
- from typing import Dict, List, Optional
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
6
  from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Path, Query, UploadFile
7
7
 
@@ -874,8 +874,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
874
874
  ) -> ConfigResponseSchema:
875
875
  knowledge = get_knowledge_instance_by_db_id(knowledge_instances, db_id)
876
876
 
877
- # Get factory readers info
878
- readers_info = get_all_readers_info()
877
+ # Get factory readers info (including custom readers from this knowledge instance)
878
+ readers_info = get_all_readers_info(knowledge)
879
879
  reader_schemas = {}
880
880
  # Add factory readers
881
881
  for reader_info in readers_info:
@@ -887,7 +887,13 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
887
887
  )
888
888
 
889
889
  # Add custom readers from knowledge.readers
890
- readers_dict: Dict[str, Reader] = knowledge.get_readers() or {}
890
+ readers_result: Any = knowledge.get_readers() or {}
891
+ print(f"readers_result: {readers_result}")
892
+ # Ensure readers_dict is a dictionary (defensive check)
893
+ if not isinstance(readers_result, dict):
894
+ readers_dict: Dict[str, Reader] = {}
895
+ else:
896
+ readers_dict = readers_result
891
897
  if readers_dict:
892
898
  for reader_id, reader in readers_dict.items():
893
899
  # Get chunking strategies from the reader
@@ -907,8 +913,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
907
913
  chunkers=chunking_strategies,
908
914
  )
909
915
 
910
- # Get content types to readers mapping
911
- types_of_readers = get_content_types_to_readers_mapping()
916
+ # Get content types to readers mapping (including custom readers from this knowledge instance)
917
+ types_of_readers = get_content_types_to_readers_mapping(knowledge)
912
918
  chunkers_list = get_all_chunkers_info()
913
919
 
914
920
  # Convert chunkers list to dictionary format expected by schema
@@ -961,20 +967,26 @@ async def process_content(
961
967
  try:
962
968
  if reader_id:
963
969
  reader = None
964
- if knowledge.readers and reader_id in knowledge.readers:
965
- reader = knowledge.readers[reader_id]
970
+ # Use get_readers() to ensure we get a dict (handles list conversion)
971
+ custom_readers = knowledge.get_readers()
972
+ if custom_readers and reader_id in custom_readers:
973
+ reader = custom_readers[reader_id]
974
+ log_debug(f"Found custom reader: {reader.__class__.__name__}")
966
975
  else:
976
+ # Try to resolve from factory readers
967
977
  key = reader_id.lower().strip().replace("-", "_").replace(" ", "_")
968
978
  candidates = [key] + ([key[:-6]] if key.endswith("reader") else [])
969
979
  for cand in candidates:
970
980
  try:
971
981
  reader = ReaderFactory.create_reader(cand)
972
- log_debug(f"Resolved reader: {reader.__class__.__name__}")
982
+ log_debug(f"Resolved reader from factory: {reader.__class__.__name__}")
973
983
  break
974
984
  except Exception:
975
985
  continue
976
986
  if reader:
977
987
  content.reader = reader
988
+ else:
989
+ log_debug(f"Could not resolve reader with id: {reader_id}")
978
990
  if chunker and content.reader:
979
991
  # Set the chunker name on the reader - let the reader handle it internally
980
992
  content.reader.set_chunking_strategy_from_string(chunker, chunk_size=chunk_size, overlap=chunk_overlap)
agno/tracing/exporter.py CHANGED
@@ -90,7 +90,7 @@ class DatabaseSpanExporter(SpanExporter):
90
90
  # Create trace record (aggregate of all spans)
91
91
  trace = create_trace_from_spans(spans)
92
92
  if trace:
93
- self.db.create_trace(trace)
93
+ self.db.upsert_trace(trace)
94
94
 
95
95
  # Create span records
96
96
  self.db.create_spans(spans)
@@ -124,7 +124,7 @@ class DatabaseSpanExporter(SpanExporter):
124
124
  # Create trace record (aggregate of all spans)
125
125
  trace = create_trace_from_spans(spans)
126
126
  if trace:
127
- create_trace_result = self.db.create_trace(trace)
127
+ create_trace_result = self.db.upsert_trace(trace)
128
128
  if create_trace_result is not None:
129
129
  await create_trace_result
130
130
 
agno/vectordb/base.py CHANGED
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from typing import Any, Dict, List, Optional
3
3
 
4
4
  from agno.knowledge.document import Document
5
+ from agno.utils.log import log_warning
5
6
  from agno.utils.string import generate_id
6
7
 
7
8
 
@@ -114,9 +115,21 @@ class VectorDb(ABC):
114
115
  def delete_by_metadata(self, metadata: Dict[str, Any]) -> bool:
115
116
  raise NotImplementedError
116
117
 
117
- @abstractmethod
118
118
  def update_metadata(self, content_id: str, metadata: Dict[str, Any]) -> None:
119
- raise NotImplementedError
119
+ """
120
+ Update the metadata for documents with the given content_id.
121
+
122
+ Default implementation logs a warning. Subclasses should override this method
123
+ to provide their specific implementation.
124
+
125
+ Args:
126
+ content_id (str): The content ID to update
127
+ metadata (Dict[str, Any]): The metadata to update
128
+ """
129
+ log_warning(
130
+ f"{self.__class__.__name__}.update_metadata() is not implemented. "
131
+ f"Metadata update for content_id '{content_id}' was skipped."
132
+ )
120
133
 
121
134
  @abstractmethod
122
135
  def delete_by_content_id(self, content_id: str) -> bool:
@@ -695,25 +695,24 @@ class PgVector(VectorDb):
695
695
  Update the metadata for a document.
696
696
 
697
697
  Args:
698
- id (str): The ID of the document.
698
+ content_id (str): The ID of the document.
699
699
  metadata (Dict[str, Any]): The metadata to update.
700
700
  """
701
+ print("metadata is: ", metadata)
701
702
  try:
702
703
  with self.Session() as sess:
703
- # Merge JSONB instead of overwriting: coalesce(existing, '{}') || :new
704
+ # Merge JSONB for metadata, but replace filters entirely (absolute value)
704
705
  stmt = (
705
706
  update(self.table)
706
707
  .where(self.table.c.content_id == content_id)
707
708
  .values(
708
709
  meta_data=func.coalesce(self.table.c.meta_data, text("'{}'::jsonb")).op("||")(
709
- bindparam("md", metadata, type_=postgresql.JSONB)
710
- ),
711
- filters=func.coalesce(self.table.c.filters, text("'{}'::jsonb")).op("||")(
712
- bindparam("ft", metadata, type_=postgresql.JSONB)
710
+ bindparam("md", type_=postgresql.JSONB)
713
711
  ),
712
+ filters=bindparam("ft", type_=postgresql.JSONB),
714
713
  )
715
714
  )
716
- sess.execute(stmt)
715
+ sess.execute(stmt, {"md": metadata, "ft": metadata})
717
716
  sess.commit()
718
717
  except Exception as e:
719
718
  log_error(f"Error updating metadata for document {content_id}: {e}")
@@ -1111,6 +1110,7 @@ class PgVector(VectorDb):
1111
1110
  search_results = self.reranker.rerank(query=query, documents=search_results)
1112
1111
 
1113
1112
  log_info(f"Found {len(search_results)} documents")
1113
+
1114
1114
  return search_results
1115
1115
  except Exception as e:
1116
1116
  log_error(f"Error during hybrid search: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 2.3.10
3
+ Version: 2.3.11
4
4
  Summary: Agno: a lightweight library for building Multi-Agent Systems
5
5
  Author-email: Ashpreet Bedi <ashpreet@agno.com>
6
6
  Project-URL: homepage, https://agno.com