agno 2.2.4__py3-none-any.whl → 2.2.6__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 (41) hide show
  1. agno/agent/agent.py +82 -19
  2. agno/culture/manager.py +3 -4
  3. agno/knowledge/chunking/agentic.py +6 -2
  4. agno/memory/manager.py +9 -4
  5. agno/models/anthropic/claude.py +1 -2
  6. agno/models/azure/ai_foundry.py +31 -14
  7. agno/models/azure/openai_chat.py +12 -4
  8. agno/models/base.py +44 -11
  9. agno/models/cerebras/cerebras.py +11 -6
  10. agno/models/groq/groq.py +7 -4
  11. agno/models/meta/llama.py +12 -6
  12. agno/models/meta/llama_openai.py +5 -1
  13. agno/models/openai/chat.py +20 -12
  14. agno/models/openai/responses.py +10 -5
  15. agno/models/utils.py +254 -8
  16. agno/models/vertexai/claude.py +9 -13
  17. agno/os/app.py +48 -21
  18. agno/os/routers/evals/evals.py +8 -8
  19. agno/os/routers/evals/utils.py +1 -0
  20. agno/os/schema.py +48 -33
  21. agno/os/utils.py +27 -0
  22. agno/run/agent.py +5 -0
  23. agno/run/team.py +2 -0
  24. agno/run/workflow.py +39 -0
  25. agno/session/summary.py +8 -2
  26. agno/session/workflow.py +4 -3
  27. agno/team/team.py +50 -14
  28. agno/tools/file.py +153 -25
  29. agno/tools/function.py +5 -1
  30. agno/tools/notion.py +201 -0
  31. agno/utils/events.py +2 -0
  32. agno/utils/print_response/workflow.py +115 -16
  33. agno/vectordb/milvus/milvus.py +5 -0
  34. agno/workflow/__init__.py +2 -0
  35. agno/workflow/agent.py +298 -0
  36. agno/workflow/workflow.py +929 -64
  37. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/METADATA +4 -1
  38. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/RECORD +41 -39
  39. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/WHEEL +0 -0
  40. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/licenses/LICENSE +0 -0
  41. {agno-2.2.4.dist-info → agno-2.2.6.dist-info}/top_level.txt +0 -0
agno/tools/notion.py ADDED
@@ -0,0 +1,201 @@
1
+ import json
2
+ import os
3
+ from typing import Any, List, Optional
4
+
5
+ from agno.tools import Toolkit
6
+ from agno.utils.log import log_debug, logger
7
+
8
+ try:
9
+ from notion_client import Client
10
+ except ImportError:
11
+ raise ImportError("`notion-client` not installed. Please install using `pip install notion-client`")
12
+
13
+
14
+ class NotionTools(Toolkit):
15
+ """
16
+ Notion toolkit for creating and managing Notion pages.
17
+
18
+ Args:
19
+ api_key (Optional[str]): Notion API key (integration token). If not provided, uses NOTION_API_KEY env var.
20
+ database_id (Optional[str]): The ID of the database to work with. If not provided, uses NOTION_DATABASE_ID env var.
21
+ enable_create_page (bool): Enable creating pages. Default is True.
22
+ enable_update_page (bool): Enable updating pages. Default is True.
23
+ enable_search_pages (bool): Enable searching pages. Default is True.
24
+ all (bool): Enable all tools. Overrides individual flags when True. Default is False.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ api_key: Optional[str] = None,
30
+ database_id: Optional[str] = None,
31
+ enable_create_page: bool = True,
32
+ enable_update_page: bool = True,
33
+ enable_search_pages: bool = True,
34
+ all: bool = False,
35
+ **kwargs,
36
+ ):
37
+ self.api_key = api_key or os.getenv("NOTION_API_KEY")
38
+ self.database_id = database_id or os.getenv("NOTION_DATABASE_ID")
39
+
40
+ if not self.api_key:
41
+ raise ValueError(
42
+ "Notion API key is required. Either pass api_key parameter or set NOTION_API_KEY environment variable."
43
+ )
44
+ if not self.database_id:
45
+ raise ValueError(
46
+ "Notion database ID is required. Either pass database_id parameter or set NOTION_DATABASE_ID environment variable."
47
+ )
48
+
49
+ self.client = Client(auth=self.api_key)
50
+
51
+ tools: List[Any] = []
52
+ if all or enable_create_page:
53
+ tools.append(self.create_page)
54
+ if all or enable_update_page:
55
+ tools.append(self.update_page)
56
+ if all or enable_search_pages:
57
+ tools.append(self.search_pages)
58
+
59
+ super().__init__(name="notion_tools", tools=tools, **kwargs)
60
+
61
+ def create_page(self, title: str, tag: str, content: str) -> str:
62
+ """Create a new page in the Notion database with a title, tag, and content.
63
+
64
+ Args:
65
+ title (str): The title of the page
66
+ tag (str): The tag/category for the page (e.g., travel, tech, general-blogs, fashion, documents)
67
+ content (str): The content to add to the page
68
+
69
+ Returns:
70
+ str: JSON string with page creation details
71
+ """
72
+ try:
73
+ log_debug(f"Creating Notion page with title: {title}, tag: {tag}")
74
+
75
+ # Create the page in the database
76
+ new_page = self.client.pages.create(
77
+ parent={"database_id": self.database_id},
78
+ properties={"Name": {"title": [{"text": {"content": title}}]}, "Tag": {"select": {"name": tag}}},
79
+ children=[
80
+ {
81
+ "object": "block",
82
+ "type": "paragraph",
83
+ "paragraph": {"rich_text": [{"type": "text", "text": {"content": content}}]},
84
+ }
85
+ ],
86
+ )
87
+
88
+ result = {"success": True, "page_id": new_page["id"], "url": new_page["url"], "title": title, "tag": tag}
89
+ return json.dumps(result, indent=2)
90
+
91
+ except Exception as e:
92
+ logger.exception(e)
93
+ return json.dumps({"success": False, "error": str(e)})
94
+
95
+ def update_page(self, page_id: str, content: str) -> str:
96
+ """Add content to an existing Notion page.
97
+
98
+ Args:
99
+ page_id (str): The ID of the page to update
100
+ content (str): The content to append to the page
101
+
102
+ Returns:
103
+ str: JSON string with update status
104
+ """
105
+ try:
106
+ log_debug(f"Updating Notion page: {page_id}")
107
+
108
+ # Append content to the page
109
+ self.client.blocks.children.append(
110
+ block_id=page_id,
111
+ children=[
112
+ {
113
+ "object": "block",
114
+ "type": "paragraph",
115
+ "paragraph": {"rich_text": [{"type": "text", "text": {"content": content}}]},
116
+ }
117
+ ],
118
+ )
119
+
120
+ result = {"success": True, "page_id": page_id, "message": "Content added successfully"}
121
+ return json.dumps(result, indent=2)
122
+
123
+ except Exception as e:
124
+ logger.exception(e)
125
+ return json.dumps({"success": False, "error": str(e)})
126
+
127
+ def search_pages(self, tag: str) -> str:
128
+ """Search for pages in the database by tag.
129
+
130
+ Args:
131
+ tag (str): The tag to search for
132
+
133
+ Returns:
134
+ str: JSON string with list of matching pages
135
+ """
136
+ try:
137
+ log_debug(f"Searching for pages with tag: {tag}")
138
+
139
+ import httpx
140
+
141
+ headers = {
142
+ "Authorization": f"Bearer {self.api_key}",
143
+ "Notion-Version": "2022-06-28",
144
+ "Content-Type": "application/json",
145
+ }
146
+
147
+ payload = {"filter": {"property": "Tag", "select": {"equals": tag}}}
148
+
149
+ # The SDK client does not support the query method
150
+ response = httpx.post(
151
+ f"https://api.notion.com/v1/databases/{self.database_id}/query",
152
+ headers=headers,
153
+ json=payload,
154
+ timeout=30.0,
155
+ )
156
+
157
+ if response.status_code != 200:
158
+ return json.dumps(
159
+ {
160
+ "success": False,
161
+ "error": f"API request failed with status {response.status_code}",
162
+ "message": response.text,
163
+ }
164
+ )
165
+
166
+ data = response.json()
167
+ pages = []
168
+
169
+ for page in data.get("results", []):
170
+ try:
171
+ page_title = "Untitled"
172
+ if page.get("properties", {}).get("Name", {}).get("title"):
173
+ page_title = page["properties"]["Name"]["title"][0]["text"]["content"]
174
+
175
+ page_tag = None
176
+ if page.get("properties", {}).get("Tag", {}).get("select"):
177
+ page_tag = page["properties"]["Tag"]["select"]["name"]
178
+
179
+ page_info = {
180
+ "page_id": page["id"],
181
+ "title": page_title,
182
+ "tag": page_tag,
183
+ "url": page.get("url", ""),
184
+ }
185
+ pages.append(page_info)
186
+ except Exception as page_error:
187
+ log_debug(f"Error parsing page: {page_error}")
188
+ continue
189
+
190
+ result = {"success": True, "count": len(pages), "pages": pages}
191
+ return json.dumps(result, indent=2)
192
+
193
+ except Exception as e:
194
+ logger.exception(e)
195
+ return json.dumps(
196
+ {
197
+ "success": False,
198
+ "error": str(e),
199
+ "message": "Failed to search pages. Make sure the database is shared with the integration and has a 'Tag' property.",
200
+ }
201
+ )
agno/utils/events.py CHANGED
@@ -106,6 +106,7 @@ def create_team_run_completed_event(from_run_response: TeamRunOutput) -> TeamRun
106
106
  member_responses=from_run_response.member_responses, # type: ignore
107
107
  metadata=from_run_response.metadata, # type: ignore
108
108
  metrics=from_run_response.metrics, # type: ignore
109
+ session_state=from_run_response.session_state, # type: ignore
109
110
  )
110
111
 
111
112
 
@@ -130,6 +131,7 @@ def create_run_completed_event(from_run_response: RunOutput) -> RunCompletedEven
130
131
  reasoning_messages=from_run_response.reasoning_messages, # type: ignore
131
132
  metadata=from_run_response.metadata, # type: ignore
132
133
  metrics=from_run_response.metrics, # type: ignore
134
+ session_state=from_run_response.session_state, # type: ignore
133
135
  )
134
136
 
135
137
 
@@ -25,6 +25,8 @@ from agno.run.workflow import (
25
25
  StepsExecutionCompletedEvent,
26
26
  StepsExecutionStartedEvent,
27
27
  StepStartedEvent,
28
+ WorkflowAgentCompletedEvent,
29
+ WorkflowAgentStartedEvent,
28
30
  WorkflowCompletedEvent,
29
31
  WorkflowErrorEvent,
30
32
  WorkflowRunOutput,
@@ -135,7 +137,16 @@ def print_response(
135
137
 
136
138
  response_timer.stop()
137
139
 
138
- if show_step_details and workflow_response.step_results:
140
+ # Check if this is a workflow agent direct response
141
+ if workflow_response.workflow_agent_run is not None and not workflow_response.workflow_agent_run.tools:
142
+ # Agent answered directly from history without executing workflow
143
+ agent_response_panel = create_panel(
144
+ content=Markdown(str(workflow_response.content)) if markdown else str(workflow_response.content),
145
+ title="Workflow Agent Response",
146
+ border_style="green",
147
+ )
148
+ console.print(agent_response_panel) # type: ignore
149
+ elif show_step_details and workflow_response.step_results:
139
150
  for i, step_output in enumerate(workflow_response.step_results):
140
151
  print_step_output_recursive(step_output, i + 1, markdown, console) # type: ignore
141
152
 
@@ -260,6 +271,8 @@ def print_response_stream(
260
271
  step_results = []
261
272
  step_started_printed = False
262
273
  is_callable_function = callable(workflow.steps)
274
+ workflow_started = False # Track if workflow has actually started
275
+ is_workflow_agent_response = False # Track if this is a workflow agent direct response
263
276
 
264
277
  # Smart step hierarchy tracking
265
278
  current_primitive_context = None # Current primitive being executed (parallel, loop, etc.)
@@ -328,12 +341,25 @@ def print_response_stream(
328
341
  ): # type: ignore
329
342
  # Handle the new event types
330
343
  if isinstance(response, WorkflowStartedEvent):
344
+ workflow_started = True
331
345
  status.update("Workflow started...")
332
346
  if is_callable_function:
333
347
  current_step_name = "Custom Function"
334
348
  current_step_index = 0
335
349
  live_log.update(status)
336
350
 
351
+ elif isinstance(response, WorkflowAgentStartedEvent):
352
+ # Workflow agent is starting to process
353
+ status.update("Workflow agent processing...")
354
+ live_log.update(status)
355
+ continue
356
+
357
+ elif isinstance(response, WorkflowAgentCompletedEvent):
358
+ # Workflow agent has completed
359
+ status.update("Workflow agent completed")
360
+ live_log.update(status)
361
+ continue
362
+
337
363
  elif isinstance(response, StepStartedEvent):
338
364
  step_name = response.step_name or "Unknown"
339
365
  step_index = response.step_index or 0 # type: ignore
@@ -646,8 +672,23 @@ def print_response_stream(
646
672
  elif isinstance(response, WorkflowCompletedEvent):
647
673
  status.update("Workflow completed!")
648
674
 
675
+ # Check if this is an agent direct response
676
+ if response.metadata and response.metadata.get("agent_direct_response"):
677
+ is_workflow_agent_response = True
678
+ # Print the agent's direct response from history
679
+ if show_step_details:
680
+ live_log.update(status, refresh=True)
681
+ agent_response_panel = create_panel(
682
+ content=Markdown(str(response.content)) if markdown else str(response.content),
683
+ title="Workflow Agent Response",
684
+ border_style="green",
685
+ )
686
+ console.print(agent_response_panel) # type: ignore
687
+ step_started_printed = True
649
688
  # For callable functions, print the final content block here since there are no step events
650
- if is_callable_function and show_step_details and current_step_content and not step_started_printed:
689
+ elif (
690
+ is_callable_function and show_step_details and current_step_content and not step_started_printed
691
+ ):
651
692
  final_step_panel = create_panel(
652
693
  content=Markdown(current_step_content) if markdown else current_step_content,
653
694
  title="Custom Function (Completed)",
@@ -658,8 +699,8 @@ def print_response_stream(
658
699
 
659
700
  live_log.update(status, refresh=True)
660
701
 
661
- # Show final summary
662
- if response.metadata:
702
+ # Show final summary (skip for agent responses)
703
+ if response.metadata and not is_workflow_agent_response:
663
704
  status = response.status
664
705
  summary_content = ""
665
706
  summary_content += f"""\n\n**Status:** {status}"""
@@ -710,8 +751,16 @@ def print_response_stream(
710
751
  and response.content_type != ""
711
752
  )
712
753
  response_str = response.content # type: ignore
754
+
755
+ if isinstance(response, RunContentEvent) and not workflow_started:
756
+ is_workflow_agent_response = True
757
+ continue
758
+
713
759
  elif isinstance(response, RunContentEvent) and current_step_executor_type != "team":
714
760
  response_str = response.content # type: ignore
761
+ # If we get RunContentEvent BEFORE workflow starts, it's an agent direct response
762
+ if not workflow_started and not is_workflow_agent_response:
763
+ is_workflow_agent_response = True
715
764
  else:
716
765
  continue
717
766
 
@@ -734,8 +783,8 @@ def print_response_stream(
734
783
  else:
735
784
  current_step_content += response_str
736
785
 
737
- # Live update the step panel with streaming content
738
- if show_step_details and not step_started_printed:
786
+ # Live update the step panel with streaming content (skip for workflow agent responses)
787
+ if show_step_details and not step_started_printed and not is_workflow_agent_response:
739
788
  # Generate smart step number for streaming title (will use cached value)
740
789
  step_display = get_step_display_number(current_step_index, current_step_name)
741
790
  title = f"{step_display}: {current_step_name} (Streaming...)"
@@ -757,8 +806,8 @@ def print_response_stream(
757
806
 
758
807
  live_log.update("")
759
808
 
760
- # Final completion message
761
- if show_time:
809
+ # Final completion message (skip for agent responses)
810
+ if show_time and not is_workflow_agent_response:
762
811
  completion_text = Text(f"Completed in {response_timer.elapsed:.1f}s", style="bold green")
763
812
  console.print(completion_text) # type: ignore
764
813
 
@@ -927,7 +976,16 @@ async def aprint_response(
927
976
 
928
977
  response_timer.stop()
929
978
 
930
- if show_step_details and workflow_response.step_results:
979
+ # Check if this is a workflow agent direct response
980
+ if workflow_response.workflow_agent_run is not None and not workflow_response.workflow_agent_run.tools:
981
+ # Agent answered directly from history without executing workflow
982
+ agent_response_panel = create_panel(
983
+ content=Markdown(str(workflow_response.content)) if markdown else str(workflow_response.content),
984
+ title="Workflow Agent Response",
985
+ border_style="green",
986
+ )
987
+ console.print(agent_response_panel) # type: ignore
988
+ elif show_step_details and workflow_response.step_results:
931
989
  for i, step_output in enumerate(workflow_response.step_results):
932
990
  print_step_output_recursive(step_output, i + 1, markdown, console) # type: ignore
933
991
 
@@ -1052,6 +1110,8 @@ async def aprint_response_stream(
1052
1110
  step_results = []
1053
1111
  step_started_printed = False
1054
1112
  is_callable_function = callable(workflow.steps)
1113
+ workflow_started = False # Track if workflow has actually started
1114
+ is_workflow_agent_response = False # Track if this is a workflow agent direct response
1055
1115
 
1056
1116
  # Smart step hierarchy tracking
1057
1117
  current_primitive_context = None # Current primitive being executed (parallel, loop, etc.)
@@ -1120,13 +1180,30 @@ async def aprint_response_stream(
1120
1180
  ): # type: ignore
1121
1181
  # Handle the new event types
1122
1182
  if isinstance(response, WorkflowStartedEvent):
1183
+ workflow_started = True
1123
1184
  status.update("Workflow started...")
1124
1185
  if is_callable_function:
1125
1186
  current_step_name = "Custom Function"
1126
1187
  current_step_index = 0
1127
1188
  live_log.update(status)
1128
1189
 
1190
+ elif isinstance(response, WorkflowAgentStartedEvent):
1191
+ # Workflow agent is starting to process
1192
+ status.update("Workflow agent processing...")
1193
+ live_log.update(status)
1194
+ continue
1195
+
1196
+ elif isinstance(response, WorkflowAgentCompletedEvent):
1197
+ # Workflow agent has completed
1198
+ status.update("Workflow agent completed")
1199
+ live_log.update(status)
1200
+ continue
1201
+
1129
1202
  elif isinstance(response, StepStartedEvent):
1203
+ # Skip step events if workflow hasn't started (agent direct response)
1204
+ if not workflow_started:
1205
+ continue
1206
+
1130
1207
  step_name = response.step_name or "Unknown"
1131
1208
  step_index = response.step_index or 0 # type: ignore
1132
1209
 
@@ -1438,8 +1515,23 @@ async def aprint_response_stream(
1438
1515
  elif isinstance(response, WorkflowCompletedEvent):
1439
1516
  status.update("Workflow completed!")
1440
1517
 
1518
+ # Check if this is an agent direct response
1519
+ if response.metadata and response.metadata.get("agent_direct_response"):
1520
+ is_workflow_agent_response = True
1521
+ # Print the agent's direct response from history
1522
+ if show_step_details:
1523
+ live_log.update(status, refresh=True)
1524
+ agent_response_panel = create_panel(
1525
+ content=Markdown(str(response.content)) if markdown else str(response.content),
1526
+ title="Workflow Agent Response",
1527
+ border_style="green",
1528
+ )
1529
+ console.print(agent_response_panel) # type: ignore
1530
+ step_started_printed = True
1441
1531
  # For callable functions, print the final content block here since there are no step events
1442
- if is_callable_function and show_step_details and current_step_content and not step_started_printed:
1532
+ elif (
1533
+ is_callable_function and show_step_details and current_step_content and not step_started_printed
1534
+ ):
1443
1535
  final_step_panel = create_panel(
1444
1536
  content=Markdown(current_step_content) if markdown else current_step_content,
1445
1537
  title="Custom Function (Completed)",
@@ -1450,8 +1542,8 @@ async def aprint_response_stream(
1450
1542
 
1451
1543
  live_log.update(status, refresh=True)
1452
1544
 
1453
- # Show final summary
1454
- if response.metadata:
1545
+ # Show final summary (skip for agent responses)
1546
+ if response.metadata and not is_workflow_agent_response:
1455
1547
  status = response.status
1456
1548
  summary_content = ""
1457
1549
  summary_content += f"""\n\n**Status:** {status}"""
@@ -1499,6 +1591,11 @@ async def aprint_response_stream(
1499
1591
  # Extract the content from the streaming event
1500
1592
  response_str = response.content # type: ignore
1501
1593
 
1594
+ # If we get RunContentEvent BEFORE workflow starts, it's an agent direct response
1595
+ if isinstance(response, RunContentEvent) and not workflow_started:
1596
+ is_workflow_agent_response = True
1597
+ continue # Skip ALL agent direct response content
1598
+
1502
1599
  # Check if this is a team's final structured output
1503
1600
  is_structured_output = (
1504
1601
  isinstance(response, TeamRunContentEvent)
@@ -1508,6 +1605,9 @@ async def aprint_response_stream(
1508
1605
  )
1509
1606
  elif isinstance(response, RunContentEvent) and current_step_executor_type != "team":
1510
1607
  response_str = response.content # type: ignore
1608
+ # If we get RunContentEvent BEFORE workflow starts, it's an agent direct response
1609
+ if not workflow_started and not is_workflow_agent_response:
1610
+ is_workflow_agent_response = True
1511
1611
  else:
1512
1612
  continue
1513
1613
 
@@ -1530,8 +1630,8 @@ async def aprint_response_stream(
1530
1630
  else:
1531
1631
  current_step_content += response_str
1532
1632
 
1533
- # Live update the step panel with streaming content
1534
- if show_step_details and not step_started_printed:
1633
+ # Live update the step panel with streaming content (skip for workflow agent responses)
1634
+ if show_step_details and not step_started_printed and not is_workflow_agent_response:
1535
1635
  # Generate smart step number for streaming title (will use cached value)
1536
1636
  step_display = get_step_display_number(current_step_index, current_step_name)
1537
1637
  title = f"{step_display}: {current_step_name} (Streaming...)"
@@ -1553,8 +1653,7 @@ async def aprint_response_stream(
1553
1653
 
1554
1654
  live_log.update("")
1555
1655
 
1556
- # Final completion message
1557
- if show_time:
1656
+ if show_time and not is_workflow_agent_response:
1558
1657
  completion_text = Text(f"Completed in {response_timer.elapsed:.1f}s", style="bold green")
1559
1658
  console.print(completion_text) # type: ignore
1560
1659
 
@@ -719,6 +719,11 @@ class Milvus(VectorDb):
719
719
  )
720
720
  )
721
721
 
722
+ # Apply reranker if available
723
+ if self.reranker and search_results:
724
+ search_results = self.reranker.rerank(query=query, documents=search_results)
725
+ search_results = search_results[:limit]
726
+
722
727
  log_info(f"Found {len(search_results)} documents")
723
728
  return search_results
724
729
 
agno/workflow/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
+ from agno.workflow.agent import WorkflowAgent
1
2
  from agno.workflow.condition import Condition
2
3
  from agno.workflow.loop import Loop
3
4
  from agno.workflow.parallel import Parallel
@@ -9,6 +10,7 @@ from agno.workflow.workflow import Workflow
9
10
 
10
11
  __all__ = [
11
12
  "Workflow",
13
+ "WorkflowAgent",
12
14
  "Steps",
13
15
  "Step",
14
16
  "Loop",