agno 2.1.9__py3-none-any.whl → 2.2.0__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.
Files changed (83) hide show
  1. agno/agent/agent.py +2048 -1204
  2. agno/culture/__init__.py +3 -0
  3. agno/culture/manager.py +954 -0
  4. agno/db/async_postgres/async_postgres.py +232 -0
  5. agno/db/async_postgres/schemas.py +15 -0
  6. agno/db/async_postgres/utils.py +58 -0
  7. agno/db/base.py +83 -6
  8. agno/db/dynamo/dynamo.py +162 -0
  9. agno/db/dynamo/schemas.py +44 -0
  10. agno/db/dynamo/utils.py +59 -0
  11. agno/db/firestore/firestore.py +231 -0
  12. agno/db/firestore/schemas.py +10 -0
  13. agno/db/firestore/utils.py +96 -0
  14. agno/db/gcs_json/gcs_json_db.py +190 -0
  15. agno/db/gcs_json/utils.py +58 -0
  16. agno/db/in_memory/in_memory_db.py +118 -0
  17. agno/db/in_memory/utils.py +58 -0
  18. agno/db/json/json_db.py +129 -0
  19. agno/db/json/utils.py +58 -0
  20. agno/db/mongo/mongo.py +222 -0
  21. agno/db/mongo/schemas.py +10 -0
  22. agno/db/mongo/utils.py +59 -0
  23. agno/db/mysql/mysql.py +232 -1
  24. agno/db/mysql/schemas.py +14 -0
  25. agno/db/mysql/utils.py +58 -0
  26. agno/db/postgres/postgres.py +242 -0
  27. agno/db/postgres/schemas.py +15 -0
  28. agno/db/postgres/utils.py +58 -0
  29. agno/db/redis/redis.py +181 -0
  30. agno/db/redis/schemas.py +14 -0
  31. agno/db/redis/utils.py +58 -0
  32. agno/db/schemas/__init__.py +2 -1
  33. agno/db/schemas/culture.py +120 -0
  34. agno/db/singlestore/schemas.py +14 -0
  35. agno/db/singlestore/singlestore.py +231 -0
  36. agno/db/singlestore/utils.py +58 -0
  37. agno/db/sqlite/schemas.py +14 -0
  38. agno/db/sqlite/sqlite.py +274 -7
  39. agno/db/sqlite/utils.py +62 -0
  40. agno/db/surrealdb/models.py +51 -1
  41. agno/db/surrealdb/surrealdb.py +154 -0
  42. agno/db/surrealdb/utils.py +61 -1
  43. agno/knowledge/reader/field_labeled_csv_reader.py +0 -2
  44. agno/memory/manager.py +28 -11
  45. agno/models/anthropic/claude.py +2 -2
  46. agno/models/message.py +0 -1
  47. agno/models/ollama/chat.py +7 -2
  48. agno/os/app.py +29 -7
  49. agno/os/interfaces/a2a/router.py +2 -2
  50. agno/os/interfaces/agui/router.py +2 -2
  51. agno/os/router.py +7 -7
  52. agno/os/routers/evals/schemas.py +31 -31
  53. agno/os/routers/health.py +6 -2
  54. agno/os/routers/knowledge/schemas.py +49 -47
  55. agno/os/routers/memory/schemas.py +16 -16
  56. agno/os/routers/metrics/schemas.py +16 -16
  57. agno/os/routers/session/session.py +382 -7
  58. agno/os/schema.py +254 -231
  59. agno/os/utils.py +1 -1
  60. agno/run/agent.py +49 -1
  61. agno/run/team.py +43 -0
  62. agno/session/summary.py +45 -13
  63. agno/session/team.py +90 -5
  64. agno/team/team.py +1118 -857
  65. agno/tools/gmail.py +59 -14
  66. agno/utils/agent.py +372 -0
  67. agno/utils/events.py +144 -2
  68. agno/utils/print_response/agent.py +10 -6
  69. agno/utils/print_response/team.py +6 -4
  70. agno/utils/print_response/workflow.py +7 -5
  71. agno/utils/team.py +9 -8
  72. agno/workflow/condition.py +17 -9
  73. agno/workflow/loop.py +18 -10
  74. agno/workflow/parallel.py +14 -6
  75. agno/workflow/router.py +17 -9
  76. agno/workflow/step.py +14 -6
  77. agno/workflow/steps.py +14 -6
  78. agno/workflow/workflow.py +245 -122
  79. {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/METADATA +60 -23
  80. {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/RECORD +83 -79
  81. {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/WHEEL +0 -0
  82. {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/licenses/LICENSE +0 -0
  83. {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ from agno.db.postgres.utils import (
7
7
  get_dates_to_calculate_metrics_for,
8
8
  )
9
9
  from agno.db.schemas import UserMemory
10
+ from agno.db.schemas.culture import CulturalKnowledge
10
11
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
11
12
  from agno.db.schemas.knowledge import KnowledgeRow
12
13
  from agno.db.surrealdb import utils
@@ -19,6 +20,7 @@ from agno.db.surrealdb.metrics import (
19
20
  )
20
21
  from agno.db.surrealdb.models import (
21
22
  TableType,
23
+ deserialize_cultural_knowledge,
22
24
  deserialize_eval_run_record,
23
25
  deserialize_knowledge_row,
24
26
  deserialize_session,
@@ -30,6 +32,7 @@ from agno.db.surrealdb.models import (
30
32
  desurrealize_user_memory,
31
33
  get_schema,
32
34
  get_session_type,
35
+ serialize_cultural_knowledge,
33
36
  serialize_eval_run_record,
34
37
  serialize_knowledge_row,
35
38
  serialize_session,
@@ -60,6 +63,7 @@ class SurrealDb(BaseDb):
60
63
  metrics_table: Optional[str] = None,
61
64
  eval_table: Optional[str] = None,
62
65
  knowledge_table: Optional[str] = None,
66
+ culture_table: Optional[str] = None,
63
67
  id: Optional[str] = None,
64
68
  ):
65
69
  """
@@ -80,6 +84,7 @@ class SurrealDb(BaseDb):
80
84
  metrics_table=metrics_table,
81
85
  eval_table=eval_table,
82
86
  knowledge_table=knowledge_table,
87
+ culture_table=culture_table,
83
88
  )
84
89
  self._client = client
85
90
  self._db_url = db_url
@@ -101,6 +106,7 @@ class SurrealDb(BaseDb):
101
106
  def table_names(self) -> dict[TableType, str]:
102
107
  return {
103
108
  "agents": self._agents_table_name,
109
+ "culture": self.culture_table_name,
104
110
  "evals": self.eval_table_name,
105
111
  "knowledge": self.knowledge_table_name,
106
112
  "memories": self.memory_table_name,
@@ -127,6 +133,8 @@ class SurrealDb(BaseDb):
127
133
  table_name = self.memory_table_name
128
134
  elif table_type == "knowledge":
129
135
  table_name = self.knowledge_table_name
136
+ elif table_type == "culture":
137
+ table_name = self.culture_table_name
130
138
  elif table_type == "users":
131
139
  table_name = self._users_table_name
132
140
  elif table_type == "agents":
@@ -463,6 +471,152 @@ class SurrealDb(BaseDb):
463
471
  table = self._get_table("memories")
464
472
  _ = self.client.delete(table)
465
473
 
474
+ # -- Cultural Knowledge methods --
475
+ def clear_cultural_knowledge(self) -> None:
476
+ """Delete all cultural knowledge from the database.
477
+
478
+ Raises:
479
+ Exception: If an error occurs during deletion.
480
+ """
481
+ table = self._get_table("culture")
482
+ _ = self.client.delete(table)
483
+
484
+ def delete_cultural_knowledge(self, id: str) -> None:
485
+ """Delete cultural knowledge by ID.
486
+
487
+ Args:
488
+ id (str): The ID of the cultural knowledge to delete.
489
+
490
+ Raises:
491
+ Exception: If an error occurs during deletion.
492
+ """
493
+ table = self._get_table("culture")
494
+ rec_id = RecordID(table, id)
495
+ self.client.delete(rec_id)
496
+
497
+ def get_cultural_knowledge(
498
+ self, id: str, deserialize: Optional[bool] = True
499
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
500
+ """Get cultural knowledge by ID.
501
+
502
+ Args:
503
+ id (str): The ID of the cultural knowledge to retrieve.
504
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
505
+
506
+ Returns:
507
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
508
+
509
+ Raises:
510
+ Exception: If an error occurs during retrieval.
511
+ """
512
+ table = self._get_table("culture")
513
+ rec_id = RecordID(table, id)
514
+ result = self.client.select(rec_id)
515
+
516
+ if result is None:
517
+ return None
518
+
519
+ if not deserialize:
520
+ return result # type: ignore
521
+
522
+ return deserialize_cultural_knowledge(result) # type: ignore
523
+
524
+ def get_all_cultural_knowledge(
525
+ self,
526
+ agent_id: Optional[str] = None,
527
+ team_id: Optional[str] = None,
528
+ name: Optional[str] = None,
529
+ limit: Optional[int] = None,
530
+ page: Optional[int] = None,
531
+ sort_by: Optional[str] = None,
532
+ sort_order: Optional[str] = None,
533
+ deserialize: Optional[bool] = True,
534
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
535
+ """Get all cultural knowledge with filtering and pagination.
536
+
537
+ Args:
538
+ agent_id (Optional[str]): Filter by agent ID.
539
+ team_id (Optional[str]): Filter by team ID.
540
+ name (Optional[str]): Filter by name (case-insensitive partial match).
541
+ limit (Optional[int]): Maximum number of results to return.
542
+ page (Optional[int]): Page number for pagination.
543
+ sort_by (Optional[str]): Field to sort by.
544
+ sort_order (Optional[str]): Sort order ('asc' or 'desc').
545
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
546
+
547
+ Returns:
548
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
549
+ - When deserialize=True: List of CulturalKnowledge objects
550
+ - When deserialize=False: Tuple with list of dictionaries and total count
551
+
552
+ Raises:
553
+ Exception: If an error occurs during retrieval.
554
+ """
555
+ table = self._get_table("culture")
556
+
557
+ # Build where clauses
558
+ where_clauses: List[WhereClause] = []
559
+ if agent_id is not None:
560
+ agent_rec_id = RecordID(self._get_table("agents"), agent_id)
561
+ where_clauses.append(("agent", "=", agent_rec_id)) # type: ignore
562
+ if team_id is not None:
563
+ team_rec_id = RecordID(self._get_table("teams"), team_id)
564
+ where_clauses.append(("team", "=", team_rec_id)) # type: ignore
565
+ if name is not None:
566
+ where_clauses.append(("string::lowercase(name)", "CONTAINS", name.lower())) # type: ignore
567
+
568
+ # Build query for total count
569
+ count_query = COUNT_QUERY.format(
570
+ table=table,
571
+ where=""
572
+ if not where_clauses
573
+ else f"WHERE {' AND '.join(f'{w[0]} {w[1]} ${chr(97 + i)}' for i, w in enumerate(where_clauses))}", # type: ignore
574
+ )
575
+ params = {chr(97 + i): w[2] for i, w in enumerate(where_clauses)} # type: ignore
576
+ total_count = self._query_one(count_query, params, int) or 0
577
+
578
+ # Build main query
579
+ order_limit = order_limit_start(sort_by, sort_order, limit, page)
580
+ query = f"SELECT * FROM {table}"
581
+ if where_clauses:
582
+ query += f" WHERE {' AND '.join(f'{w[0]} {w[1]} ${chr(97 + i)}' for i, w in enumerate(where_clauses))}" # type: ignore
583
+ query += order_limit
584
+
585
+ results = self._query(query, params, list) or []
586
+
587
+ if not deserialize:
588
+ return results, total_count # type: ignore
589
+
590
+ return [deserialize_cultural_knowledge(r) for r in results] # type: ignore
591
+
592
+ def upsert_cultural_knowledge(
593
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
594
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
595
+ """Upsert cultural knowledge in SurrealDB.
596
+
597
+ Args:
598
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
599
+ deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
600
+
601
+ Returns:
602
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
603
+
604
+ Raises:
605
+ Exception: If an error occurs during upsert.
606
+ """
607
+ table = self._get_table("culture", create_table_if_not_found=True)
608
+ serialized = serialize_cultural_knowledge(cultural_knowledge, table)
609
+
610
+ result = self.client.upsert(serialized["id"], serialized)
611
+
612
+ if result is None:
613
+ return None
614
+
615
+ if not deserialize:
616
+ return result # type: ignore
617
+
618
+ return deserialize_cultural_knowledge(result) # type: ignore
619
+
466
620
  def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None) -> None:
467
621
  """Delete a user memory from the database.
468
622
 
@@ -1,8 +1,9 @@
1
1
  import dataclasses
2
- from typing import Any, Optional, Sequence, TypeVar, Union, cast
2
+ from typing import Any, Dict, Optional, Sequence, TypeVar, Union, cast
3
3
 
4
4
  from surrealdb import BlockingHttpSurrealConnection, BlockingWsSurrealConnection, Surreal
5
5
 
6
+ from agno.db.schemas.culture import CulturalKnowledge
6
7
  from agno.utils.log import logger
7
8
 
8
9
  RecordType = TypeVar("RecordType")
@@ -85,3 +86,62 @@ def query_one(
85
86
  raise ValueError(f"Expected single record, got {len(response)} records: {response}")
86
87
  else:
87
88
  raise ValueError(f"Unexpected response type: {type(response)}")
89
+
90
+
91
+ # -- Cultural Knowledge util methods --
92
+
93
+
94
+ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
95
+ """Serialize a CulturalKnowledge object for database storage.
96
+
97
+ Converts the model's separate content, categories, and notes fields
98
+ into a single dict for the database content field.
99
+
100
+ Args:
101
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
102
+
103
+ Returns:
104
+ Dict[str, Any]: A dictionary with content, categories, and notes.
105
+ """
106
+ content_dict: Dict[str, Any] = {}
107
+ if cultural_knowledge.content is not None:
108
+ content_dict["content"] = cultural_knowledge.content
109
+ if cultural_knowledge.categories is not None:
110
+ content_dict["categories"] = cultural_knowledge.categories
111
+ if cultural_knowledge.notes is not None:
112
+ content_dict["notes"] = cultural_knowledge.notes
113
+
114
+ return content_dict if content_dict else {}
115
+
116
+
117
+ def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
118
+ """Deserialize a database row to a CulturalKnowledge object.
119
+
120
+ The database stores content as a dict containing content, categories, and notes.
121
+ This method extracts those fields and converts them back to the model format.
122
+
123
+ Args:
124
+ db_row (Dict[str, Any]): The database row as a dictionary.
125
+
126
+ Returns:
127
+ CulturalKnowledge: The cultural knowledge object.
128
+ """
129
+ # Extract content, categories, and notes from the content field
130
+ content_json = db_row.get("content", {}) or {}
131
+
132
+ return CulturalKnowledge.from_dict(
133
+ {
134
+ "id": db_row.get("id"),
135
+ "name": db_row.get("name"),
136
+ "summary": db_row.get("summary"),
137
+ "content": content_json.get("content"),
138
+ "categories": content_json.get("categories"),
139
+ "notes": content_json.get("notes"),
140
+ "metadata": db_row.get("metadata"),
141
+ "input": db_row.get("input"),
142
+ "created_at": db_row.get("created_at"),
143
+ "updated_at": db_row.get("updated_at"),
144
+ "agent_id": db_row.get("agent_id"),
145
+ "team_id": db_row.get("team_id"),
146
+ }
147
+ )
@@ -33,8 +33,6 @@ class FieldLabeledCSVReader(Reader):
33
33
  self.format_headers = format_headers
34
34
  self.skip_empty_fields = skip_empty_fields
35
35
 
36
- logger.info(f"FieldLabeledCSVReader initialized - chunk_title: {chunk_title}, field_names: {self.field_names}")
37
-
38
36
  @classmethod
39
37
  def get_supported_chunking_strategies(cls) -> List[ChunkingStrategyType]:
40
38
  """Chunking is not supported - each row is already a logical document unit."""
agno/memory/manager.py CHANGED
@@ -12,7 +12,13 @@ from agno.db.schemas import UserMemory
12
12
  from agno.models.base import Model
13
13
  from agno.models.message import Message
14
14
  from agno.tools.function import Function
15
- from agno.utils.log import log_debug, log_error, log_warning, set_log_level_to_debug, set_log_level_to_info
15
+ from agno.utils.log import (
16
+ log_debug,
17
+ log_error,
18
+ log_warning,
19
+ set_log_level_to_debug,
20
+ set_log_level_to_info,
21
+ )
16
22
  from agno.utils.prompts import get_json_output_prompt
17
23
  from agno.utils.string import parse_response_model_str
18
24
 
@@ -21,7 +27,8 @@ class MemorySearchResponse(BaseModel):
21
27
  """Model for Memory Search Response."""
22
28
 
23
29
  memory_ids: List[str] = Field(
24
- ..., description="The IDs of the memories that are most semantically similar to the query."
30
+ ...,
31
+ description="The IDs of the memories that are most semantically similar to the query.",
25
32
  )
26
33
 
27
34
 
@@ -32,11 +39,11 @@ class MemoryManager:
32
39
  # Model used for memory management
33
40
  model: Optional[Model] = None
34
41
 
35
- # Provide the system message for the manager as a string. If not provided, a default prompt will be used.
42
+ # Provide the system message for the manager as a string. If not provided, the default system message will be used.
36
43
  system_message: Optional[str] = None
37
- # Provide the memory capture instructions for the manager as a string. If not provided, a default prompt will be used.
44
+ # Provide the memory capture instructions for the manager as a string. If not provided, the default memory capture instructions will be used.
38
45
  memory_capture_instructions: Optional[str] = None
39
- # Additional instructions for the manager. These instructions are appended to the default system prompt.
46
+ # Additional instructions for the manager. These instructions are appended to the default system message.
40
47
  additional_instructions: Optional[str] = None
41
48
 
42
49
  # Whether memories were created in the last run
@@ -731,14 +738,16 @@ class MemoryManager:
731
738
  if self.system_message is not None:
732
739
  return Message(role="system", content=self.system_message)
733
740
 
734
- memory_capture_instructions = self.memory_capture_instructions or dedent("""\
741
+ memory_capture_instructions = self.memory_capture_instructions or dedent(
742
+ """\
735
743
  Memories should capture personal information about the user that is relevant to the current conversation, such as:
736
744
  - Personal facts: name, age, occupation, location, interests, and preferences
737
745
  - Opinions and preferences: what the user likes, dislikes, enjoys, or finds frustrating
738
746
  - Significant life events or experiences shared by the user
739
747
  - Important context about the user's current situation, challenges, or goals
740
748
  - Any other details that offer meaningful insight into the user's personality, perspective, or needs
741
- """)
749
+ """
750
+ )
742
751
 
743
752
  # -*- Return a system message for the memory manager
744
753
  system_prompt_lines = [
@@ -852,7 +861,9 @@ class MemoryManager:
852
861
 
853
862
  # Generate a response from the Model (includes running function calls)
854
863
  response = model_copy.response(
855
- messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
864
+ messages=messages_for_model,
865
+ tools=self._tools_for_model,
866
+ functions=self._functions_for_model,
856
867
  )
857
868
 
858
869
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -928,7 +939,9 @@ class MemoryManager:
928
939
 
929
940
  # Generate a response from the Model (includes running function calls)
930
941
  response = await model_copy.aresponse(
931
- messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
942
+ messages=messages_for_model,
943
+ tools=self._tools_for_model,
944
+ functions=self._functions_for_model,
932
945
  )
933
946
 
934
947
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -983,7 +996,9 @@ class MemoryManager:
983
996
 
984
997
  # Generate a response from the Model (includes running function calls)
985
998
  response = model_copy.response(
986
- messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
999
+ messages=messages_for_model,
1000
+ tools=self._tools_for_model,
1001
+ functions=self._functions_for_model,
987
1002
  )
988
1003
 
989
1004
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -1051,7 +1066,9 @@ class MemoryManager:
1051
1066
 
1052
1067
  # Generate a response from the Model (includes running function calls)
1053
1068
  response = await model_copy.aresponse(
1054
- messages=messages_for_model, tools=self._tools_for_model, functions=self._functions_for_model
1069
+ messages=messages_for_model,
1070
+ tools=self._tools_for_model,
1071
+ functions=self._functions_for_model,
1055
1072
  )
1056
1073
 
1057
1074
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -75,7 +75,7 @@ class Claude(Model):
75
75
  provider: str = "Anthropic"
76
76
 
77
77
  # Request parameters
78
- max_tokens: Optional[int] = 4096
78
+ max_tokens: Optional[int] = 8192
79
79
  thinking: Optional[Dict[str, Any]] = None
80
80
  temperature: Optional[float] = None
81
81
  stop_sequences: Optional[List[str]] = None
@@ -656,7 +656,7 @@ class Claude(Model):
656
656
 
657
657
  # Anthropic-specific additional fields
658
658
  if response_usage.server_tool_use:
659
- metrics.provider_metrics = {"server_tool_use": response_usage.server_tool_use}
659
+ metrics.provider_metrics = {"server_tool_use": response_usage.server_tool_use.model_dump()}
660
660
  if isinstance(response_usage, Usage):
661
661
  if response_usage.service_tier:
662
662
  metrics.provider_metrics = metrics.provider_metrics or {}
agno/models/message.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import json
2
2
  from time import time
3
3
  from typing import Any, Dict, List, Optional, Sequence, Union
4
-
5
4
  from uuid import uuid4
6
5
 
7
6
  from pydantic import BaseModel, ConfigDict, Field
@@ -429,8 +429,13 @@ class Ollama(Model):
429
429
  """
430
430
  metrics = Metrics()
431
431
 
432
- metrics.input_tokens = response.get("prompt_eval_count", 0)
433
- metrics.output_tokens = response.get("eval_count", 0)
432
+ # Safely handle None values from Ollama Cloud responses
433
+ input_tokens = response.get("prompt_eval_count")
434
+ output_tokens = response.get("eval_count")
435
+
436
+ # Default to 0 if None
437
+ metrics.input_tokens = input_tokens if input_tokens is not None else 0
438
+ metrics.output_tokens = output_tokens if output_tokens is not None else 0
434
439
  metrics.total_tokens = metrics.input_tokens + metrics.output_tokens
435
440
 
436
441
  return metrics
agno/os/app.py CHANGED
@@ -13,6 +13,7 @@ from starlette.requests import Request
13
13
 
14
14
  from agno.agent.agent import Agent
15
15
  from agno.db.base import AsyncBaseDb, BaseDb
16
+ from agno.knowledge.knowledge import Knowledge
16
17
  from agno.os.config import (
17
18
  AgentOSConfig,
18
19
  DatabaseConfig,
@@ -98,6 +99,7 @@ class AgentOS:
98
99
  agents: Optional[List[Agent]] = None,
99
100
  teams: Optional[List[Team]] = None,
100
101
  workflows: Optional[List[Workflow]] = None,
102
+ knowledge: Optional[List[Knowledge]] = None,
101
103
  interfaces: Optional[List[BaseInterface]] = None,
102
104
  a2a_interface: bool = False,
103
105
  config: Optional[Union[str, AgentOSConfig]] = None,
@@ -122,6 +124,7 @@ class AgentOS:
122
124
  agents: List of agents to include in the OS
123
125
  teams: List of teams to include in the OS
124
126
  workflows: List of workflows to include in the OS
127
+ knowledge: List of knowledge bases to include in the OS
125
128
  interfaces: List of interfaces to include in the OS
126
129
  a2a_interface: Whether to expose the OS agents and teams in an A2A server
127
130
  config: Configuration file path or AgentOSConfig instance
@@ -133,8 +136,8 @@ class AgentOS:
133
136
  telemetry: Whether to enable telemetry
134
137
 
135
138
  """
136
- if not agents and not workflows and not teams:
137
- raise ValueError("Either agents, teams or workflows must be provided.")
139
+ if not agents and not workflows and not teams and not knowledge:
140
+ raise ValueError("Either agents, teams, workflows or knowledge bases must be provided.")
138
141
 
139
142
  self.config = load_yaml_config(config) if isinstance(config, str) else config
140
143
 
@@ -143,7 +146,7 @@ class AgentOS:
143
146
  self.teams: Optional[List[Team]] = teams
144
147
  self.interfaces = interfaces or []
145
148
  self.a2a_interface = a2a_interface
146
-
149
+ self.knowledge = knowledge
147
150
  self.settings: AgnoAPISettings = settings or AgnoAPISettings()
148
151
 
149
152
  self._app_set = False
@@ -309,7 +312,7 @@ class AgentOS:
309
312
  async with self._mcp_app.lifespan(app): # type: ignore
310
313
  yield
311
314
 
312
- final_lifespan = combined_lifespan
315
+ final_lifespan = combined_lifespan # type: ignore
313
316
 
314
317
  fastapi_app = self._make_app(lifespan=final_lifespan)
315
318
  else:
@@ -480,6 +483,10 @@ class AgentOS:
480
483
  if workflow.db:
481
484
  self._register_db_with_validation(dbs, workflow.db)
482
485
 
486
+ for knowledge_base in self.knowledge or []:
487
+ if knowledge_base.contents_db:
488
+ self._register_db_with_validation(knowledge_dbs, knowledge_base.contents_db)
489
+
483
490
  for interface in self.interfaces or []:
484
491
  if interface.agent and interface.agent.db:
485
492
  self._register_db_with_validation(dbs, interface.agent.db)
@@ -534,14 +541,29 @@ class AgentOS:
534
541
 
535
542
  def _auto_discover_knowledge_instances(self) -> None:
536
543
  """Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
537
- knowledge_instances = []
544
+ seen_ids = set()
545
+ knowledge_instances: List[Knowledge] = []
546
+
547
+ def _add_knowledge_if_not_duplicate(knowledge: "Knowledge") -> None:
548
+ """Add knowledge instance if it's not already in the list (by object identity or db_id)."""
549
+ # Use database ID if available, otherwise use object ID as fallback
550
+ if not knowledge.contents_db:
551
+ return
552
+ if knowledge.contents_db.id in seen_ids:
553
+ return
554
+ seen_ids.add(knowledge.contents_db.id)
555
+ knowledge_instances.append(knowledge)
556
+
538
557
  for agent in self.agents or []:
539
558
  if agent.knowledge:
540
- knowledge_instances.append(agent.knowledge)
559
+ _add_knowledge_if_not_duplicate(agent.knowledge)
541
560
 
542
561
  for team in self.teams or []:
543
562
  if team.knowledge:
544
- knowledge_instances.append(team.knowledge)
563
+ _add_knowledge_if_not_duplicate(team.knowledge)
564
+
565
+ for knowledge_base in self.knowledge or []:
566
+ _add_knowledge_if_not_duplicate(knowledge_base)
545
567
 
546
568
  self.knowledge_instances = knowledge_instances
547
569
 
@@ -221,7 +221,7 @@ def attach_routes(
221
221
  session_id=context_id,
222
222
  user_id=user_id,
223
223
  stream=True,
224
- stream_intermediate_steps=True,
224
+ stream_events=True,
225
225
  **kwargs,
226
226
  )
227
227
  else:
@@ -234,7 +234,7 @@ def attach_routes(
234
234
  session_id=context_id,
235
235
  user_id=user_id,
236
236
  stream=True,
237
- stream_intermediate_steps=True,
237
+ stream_events=True,
238
238
  **kwargs,
239
239
  )
240
240
 
@@ -44,7 +44,7 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
44
44
  input=messages,
45
45
  session_id=run_input.thread_id,
46
46
  stream=True,
47
- stream_intermediate_steps=True,
47
+ stream_events=True,
48
48
  user_id=user_id,
49
49
  )
50
50
 
@@ -80,7 +80,7 @@ async def run_team(team: Team, input: RunAgentInput) -> AsyncIterator[BaseEvent]
80
80
  input=messages,
81
81
  session_id=input.thread_id,
82
82
  stream=True,
83
- stream_intermediate_steps=True,
83
+ stream_steps=True,
84
84
  user_id=user_id,
85
85
  )
86
86
 
agno/os/router.py CHANGED
@@ -250,7 +250,7 @@ async def agent_response_streamer(
250
250
  videos=videos,
251
251
  files=files,
252
252
  stream=True,
253
- stream_intermediate_steps=True,
253
+ stream_events=True,
254
254
  **kwargs,
255
255
  )
256
256
  async for run_response_chunk in run_response:
@@ -287,7 +287,7 @@ async def agent_continue_response_streamer(
287
287
  session_id=session_id,
288
288
  user_id=user_id,
289
289
  stream=True,
290
- stream_intermediate_steps=True,
290
+ stream_events=True,
291
291
  )
292
292
  async for run_response_chunk in continue_response:
293
293
  yield format_sse_event(run_response_chunk) # type: ignore
@@ -335,7 +335,7 @@ async def team_response_streamer(
335
335
  videos=videos,
336
336
  files=files,
337
337
  stream=True,
338
- stream_intermediate_steps=True,
338
+ stream_events=True,
339
339
  **kwargs,
340
340
  )
341
341
  async for run_response_chunk in run_response:
@@ -389,12 +389,12 @@ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os:
389
389
  session_id = str(uuid4())
390
390
 
391
391
  # Execute workflow in background with streaming
392
- workflow_result = await workflow.arun(
392
+ workflow_result = await workflow.arun( # type: ignore
393
393
  input=user_message,
394
394
  session_id=session_id,
395
395
  user_id=user_id,
396
396
  stream=True,
397
- stream_intermediate_steps=True,
397
+ stream_events=True,
398
398
  background=True,
399
399
  websocket=websocket,
400
400
  )
@@ -435,12 +435,12 @@ async def workflow_response_streamer(
435
435
  **kwargs: Any,
436
436
  ) -> AsyncGenerator:
437
437
  try:
438
- run_response = await workflow.arun(
438
+ run_response = workflow.arun(
439
439
  input=input,
440
440
  session_id=session_id,
441
441
  user_id=user_id,
442
442
  stream=True,
443
- stream_intermediate_steps=True,
443
+ stream_events=True,
444
444
  **kwargs,
445
445
  )
446
446