agno 1.7.3__py3-none-any.whl → 1.7.5__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 (59) hide show
  1. agno/agent/agent.py +113 -31
  2. agno/api/schemas/agent.py +1 -0
  3. agno/api/schemas/team.py +1 -0
  4. agno/app/fastapi/app.py +1 -1
  5. agno/app/fastapi/async_router.py +67 -16
  6. agno/app/fastapi/sync_router.py +80 -14
  7. agno/app/playground/app.py +2 -1
  8. agno/app/playground/async_router.py +97 -28
  9. agno/app/playground/operator.py +25 -19
  10. agno/app/playground/schemas.py +1 -0
  11. agno/app/playground/sync_router.py +93 -26
  12. agno/knowledge/agent.py +39 -2
  13. agno/knowledge/combined.py +1 -1
  14. agno/run/base.py +2 -0
  15. agno/run/response.py +4 -4
  16. agno/run/team.py +6 -6
  17. agno/run/v2/__init__.py +0 -0
  18. agno/run/v2/workflow.py +563 -0
  19. agno/storage/base.py +4 -4
  20. agno/storage/dynamodb.py +74 -10
  21. agno/storage/firestore.py +6 -1
  22. agno/storage/gcs_json.py +8 -2
  23. agno/storage/json.py +20 -5
  24. agno/storage/mongodb.py +14 -5
  25. agno/storage/mysql.py +56 -17
  26. agno/storage/postgres.py +55 -13
  27. agno/storage/redis.py +25 -5
  28. agno/storage/session/__init__.py +3 -1
  29. agno/storage/session/agent.py +3 -0
  30. agno/storage/session/team.py +3 -0
  31. agno/storage/session/v2/__init__.py +5 -0
  32. agno/storage/session/v2/workflow.py +89 -0
  33. agno/storage/singlestore.py +74 -12
  34. agno/storage/sqlite.py +64 -18
  35. agno/storage/yaml.py +26 -6
  36. agno/team/team.py +105 -21
  37. agno/tools/decorator.py +45 -2
  38. agno/tools/function.py +16 -12
  39. agno/utils/log.py +12 -0
  40. agno/utils/message.py +5 -1
  41. agno/utils/openai.py +20 -5
  42. agno/utils/pprint.py +34 -8
  43. agno/vectordb/surrealdb/__init__.py +3 -0
  44. agno/vectordb/surrealdb/surrealdb.py +493 -0
  45. agno/workflow/v2/__init__.py +21 -0
  46. agno/workflow/v2/condition.py +554 -0
  47. agno/workflow/v2/loop.py +602 -0
  48. agno/workflow/v2/parallel.py +659 -0
  49. agno/workflow/v2/router.py +521 -0
  50. agno/workflow/v2/step.py +861 -0
  51. agno/workflow/v2/steps.py +465 -0
  52. agno/workflow/v2/types.py +347 -0
  53. agno/workflow/v2/workflow.py +3132 -0
  54. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/METADATA +4 -1
  55. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/RECORD +59 -44
  56. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/WHEEL +0 -0
  57. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/entry_points.txt +0 -0
  58. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/licenses/LICENSE +0 -0
  59. {agno-1.7.3.dist-info → agno-1.7.5.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  from dataclasses import asdict
3
3
  from io import BytesIO
4
- from typing import Any, Dict, Generator, List, Optional, cast
4
+ from typing import Any, Dict, Generator, List, Optional, Union, cast
5
5
  from uuid import uuid4
6
6
 
7
7
  from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile
@@ -15,8 +15,10 @@ from agno.run.base import RunStatus
15
15
  from agno.run.response import RunResponseEvent
16
16
  from agno.run.team import RunResponseErrorEvent as TeamRunResponseErrorEvent
17
17
  from agno.run.team import TeamRunResponseEvent
18
+ from agno.run.v2.workflow import WorkflowErrorEvent
18
19
  from agno.team.team import Team
19
20
  from agno.utils.log import logger
21
+ from agno.workflow.v2.workflow import Workflow as WorkflowV2
20
22
  from agno.workflow.workflow import Workflow
21
23
 
22
24
 
@@ -82,6 +84,42 @@ def team_chat_response_streamer(
82
84
  return
83
85
 
84
86
 
87
+ def workflow_response_streamer(
88
+ workflow: WorkflowV2,
89
+ body: Union[Dict[str, Any], str],
90
+ session_id: Optional[str] = None,
91
+ user_id: Optional[str] = None,
92
+ ) -> Generator:
93
+ try:
94
+ if isinstance(body, dict):
95
+ run_response = workflow.run(
96
+ **body,
97
+ user_id=user_id,
98
+ session_id=session_id,
99
+ stream=True,
100
+ stream_intermediate_steps=True,
101
+ )
102
+ else:
103
+ run_response = workflow.run(
104
+ body,
105
+ user_id=user_id,
106
+ session_id=session_id,
107
+ stream=True,
108
+ stream_intermediate_steps=True,
109
+ )
110
+ for run_response_chunk in run_response:
111
+ yield run_response_chunk.to_json()
112
+ except Exception as e:
113
+ import traceback
114
+
115
+ traceback.print_exc(limit=3)
116
+ error_response = WorkflowErrorEvent(
117
+ error=str(e),
118
+ )
119
+ yield error_response.to_json()
120
+ return
121
+
122
+
85
123
  def get_sync_router(
86
124
  agents: Optional[List[Agent]] = None, teams: Optional[List[Team]] = None, workflows: Optional[List[Workflow]] = None
87
125
  ) -> APIRouter:
@@ -251,12 +289,12 @@ def get_sync_router(
251
289
  @router.post("/runs")
252
290
  def run_agent_or_team_or_workflow(
253
291
  message: str = Form(None),
254
- stream: bool = Form(True),
292
+ stream: bool = Form(False),
255
293
  monitor: bool = Form(False),
256
294
  agent_id: Optional[str] = Query(None),
257
295
  team_id: Optional[str] = Query(None),
258
296
  workflow_id: Optional[str] = Query(None),
259
- workflow_input: Optional[Dict[str, Any]] = Form(None),
297
+ workflow_input: Optional[str] = Form(None),
260
298
  session_id: Optional[str] = Form(None),
261
299
  user_id: Optional[str] = Form(None),
262
300
  files: Optional[List[UploadFile]] = File(None),
@@ -297,6 +335,13 @@ def get_sync_router(
297
335
  if not workflow_input:
298
336
  raise HTTPException(status_code=400, detail="Workflow input is required")
299
337
 
338
+ # Parse workflow_input into a dict if it is a valid JSON
339
+ try:
340
+ parsed_workflow_input = json.loads(workflow_input)
341
+ workflow_input = parsed_workflow_input
342
+ except json.JSONDecodeError:
343
+ pass
344
+
300
345
  if agent:
301
346
  agent.monitoring = bool(monitor)
302
347
  elif team:
@@ -339,13 +384,25 @@ def get_sync_router(
339
384
  media_type="text/event-stream",
340
385
  )
341
386
  elif workflow:
342
- workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id})
343
- workflow_instance.user_id = user_id
344
- workflow_instance.session_name = None
345
- return StreamingResponse(
346
- (json.dumps(asdict(result)) for result in workflow_instance.run(**(workflow_input or {}))),
347
- media_type="text/event-stream",
348
- )
387
+ if isinstance(workflow, Workflow):
388
+ workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id})
389
+ workflow_instance.user_id = user_id
390
+ workflow_instance.session_name = None
391
+ if isinstance(workflow_input, dict):
392
+ return StreamingResponse(
393
+ (json.dumps(asdict(result)) for result in workflow_instance.run(**workflow_input)),
394
+ media_type="text/event-stream",
395
+ )
396
+ else:
397
+ return StreamingResponse(
398
+ (json.dumps(asdict(result)) for result in workflow_instance.run(workflow_input)), # type: ignore
399
+ media_type="text/event-stream",
400
+ )
401
+ else:
402
+ return StreamingResponse(
403
+ workflow_response_streamer(workflow, workflow_input, session_id=session_id, user_id=user_id),
404
+ media_type="text/event-stream",
405
+ )
349
406
  else:
350
407
  if agent:
351
408
  run_response = cast(
@@ -374,9 +431,18 @@ def get_sync_router(
374
431
  )
375
432
  return team_run_response.to_dict()
376
433
  elif workflow:
377
- workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id})
378
- workflow_instance.user_id = user_id
379
- workflow_instance.session_name = None
380
- return workflow_instance.run(**(workflow_input or {})).to_dict()
434
+ if isinstance(workflow, Workflow):
435
+ workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id})
436
+ workflow_instance.user_id = user_id
437
+ workflow_instance.session_name = None
438
+ if isinstance(workflow_input, dict):
439
+ return workflow_instance.run(**workflow_input).to_dict()
440
+ else:
441
+ return workflow_instance.run(workflow_input).to_dict() # type: ignore
442
+ else:
443
+ if isinstance(workflow_input, dict):
444
+ return workflow.run(**workflow_input, session_id=session_id, user_id=user_id).to_dict()
445
+ else:
446
+ return workflow.run(workflow_input, session_id=session_id, user_id=user_id).to_dict()
381
447
 
382
448
  return router
@@ -83,7 +83,7 @@ class Playground:
83
83
 
84
84
  if self.workflows:
85
85
  for workflow in self.workflows:
86
- if not workflow.app_id:
86
+ if hasattr(workflow, "app_id") and not workflow.app_id:
87
87
  workflow.app_id = self.app_id
88
88
  if not workflow.workflow_id:
89
89
  workflow.workflow_id = generate_id(workflow.name)
@@ -197,6 +197,7 @@ class Playground:
197
197
  # Print the panel
198
198
  console.print(panel)
199
199
  self.set_app_id()
200
+
200
201
  self.register_app_on_platform()
201
202
 
202
203
  uvicorn.run(app=app, host=host, port=port, reload=reload, **kwargs)
@@ -38,11 +38,13 @@ from agno.memory.agent import AgentMemory
38
38
  from agno.memory.v2 import Memory
39
39
  from agno.run.response import RunResponseErrorEvent, RunResponseEvent
40
40
  from agno.run.team import RunResponseErrorEvent as TeamRunResponseErrorEvent
41
+ from agno.run.v2.workflow import WorkflowErrorEvent
41
42
  from agno.storage.session.agent import AgentSession
42
43
  from agno.storage.session.team import TeamSession
43
44
  from agno.storage.session.workflow import WorkflowSession
44
45
  from agno.team.team import Team
45
46
  from agno.utils.log import logger
47
+ from agno.workflow.v2.workflow import Workflow as WorkflowV2
46
48
  from agno.workflow.workflow import Workflow
47
49
 
48
50
 
@@ -146,6 +148,31 @@ async def team_chat_response_streamer(
146
148
  return
147
149
 
148
150
 
151
+ async def workflow_response_streamer(
152
+ workflow: WorkflowV2,
153
+ body: WorkflowRunRequest,
154
+ ) -> AsyncGenerator:
155
+ try:
156
+ run_response = await workflow.arun(
157
+ **body.input,
158
+ user_id=body.user_id,
159
+ session_id=body.session_id or str(uuid4()),
160
+ stream=True,
161
+ stream_intermediate_steps=True,
162
+ )
163
+ async for run_response_chunk in run_response:
164
+ yield run_response_chunk.to_json()
165
+ except Exception as e:
166
+ import traceback
167
+
168
+ traceback.print_exc(limit=3)
169
+ error_response = WorkflowErrorEvent(
170
+ error=str(e),
171
+ )
172
+ yield error_response.to_json()
173
+ return
174
+
175
+
149
176
  def get_async_playground_router(
150
177
  agents: Optional[List[Agent]] = None,
151
178
  workflows: Optional[List[Workflow]] = None,
@@ -616,13 +643,22 @@ def get_async_playground_router(
616
643
  if workflow is None:
617
644
  raise HTTPException(status_code=404, detail="Workflow not found")
618
645
 
619
- return WorkflowGetResponse(
620
- workflow_id=workflow.workflow_id,
621
- name=workflow.name,
622
- description=workflow.description,
623
- parameters=workflow._run_parameters or {},
624
- storage=workflow.storage.__class__.__name__ if workflow.storage else None,
625
- )
646
+ if isinstance(workflow, Workflow):
647
+ return WorkflowGetResponse(
648
+ workflow_id=workflow.workflow_id,
649
+ name=workflow.name,
650
+ description=workflow.description,
651
+ parameters=workflow._run_parameters or {},
652
+ storage=workflow.storage.__class__.__name__ if workflow.storage else None,
653
+ )
654
+ else:
655
+ return WorkflowGetResponse(
656
+ workflow_id=workflow.workflow_id,
657
+ name=workflow.name,
658
+ description=workflow.description,
659
+ parameters=workflow.run_parameters,
660
+ storage=workflow.storage.__class__.__name__ if workflow.storage else None,
661
+ )
626
662
 
627
663
  @playground_router.post("/workflows/{workflow_id}/runs")
628
664
  async def create_workflow_run(workflow_id: str, body: WorkflowRunRequest):
@@ -637,24 +673,54 @@ def get_async_playground_router(
637
673
  logger.debug("Creating new session")
638
674
 
639
675
  # Create a new instance of this workflow
640
- new_workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id, "session_id": body.session_id})
641
- new_workflow_instance.user_id = body.user_id
642
- new_workflow_instance.session_name = None
676
+ if isinstance(workflow, Workflow):
677
+ new_workflow_instance = workflow.deep_copy(
678
+ update={"workflow_id": workflow_id, "session_id": body.session_id}
679
+ )
680
+ new_workflow_instance.user_id = body.user_id
681
+ new_workflow_instance.session_name = None
643
682
 
644
- # Return based on the response type
645
- try:
646
- if new_workflow_instance._run_return_type == "RunResponse":
647
- # Return as a normal response
648
- return new_workflow_instance.run(**body.input)
649
- else:
650
- # Return as a streaming response
651
- return StreamingResponse(
652
- (result.to_json() for result in new_workflow_instance.run(**body.input)),
653
- media_type="text/event-stream",
654
- )
655
- except Exception as e:
656
- # Handle unexpected runtime errors
657
- raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
683
+ # Return based on the response type
684
+ try:
685
+ if new_workflow_instance._run_return_type == "RunResponse":
686
+ # Return as a normal response
687
+ return new_workflow_instance.run(**body.input)
688
+ else:
689
+ # Return as a streaming response
690
+ return StreamingResponse(
691
+ (result.to_json() for result in new_workflow_instance.run(**body.input)),
692
+ media_type="text/event-stream",
693
+ headers={
694
+ "Access-Control-Allow-Origin": "*",
695
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
696
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
697
+ },
698
+ )
699
+ except Exception as e:
700
+ # Handle unexpected runtime errors
701
+ raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
702
+ else:
703
+ # Return based on the response type
704
+ try:
705
+ if body.stream:
706
+ # Return as a streaming response
707
+ return StreamingResponse(
708
+ workflow_response_streamer(workflow, body),
709
+ media_type="text/event-stream",
710
+ headers={
711
+ "Access-Control-Allow-Origin": "*",
712
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
713
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
714
+ },
715
+ )
716
+ else:
717
+ # Return as a normal response
718
+ return await workflow.arun(
719
+ **body.input, session_id=body.session_id or str(uuid4()), user_id=body.user_id
720
+ )
721
+ except Exception as e:
722
+ # Handle unexpected runtime errors
723
+ raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
658
724
 
659
725
  @playground_router.get("/workflows/{workflow_id}/sessions")
660
726
  async def get_all_workflow_sessions(workflow_id: str, user_id: Optional[str] = Query(None, min_length=1)):
@@ -689,7 +755,7 @@ def get_async_playground_router(
689
755
  )
690
756
  return workflow_sessions
691
757
 
692
- @playground_router.get("/workflows/{workflow_id}/sessions/{session_id}", response_model=WorkflowSession)
758
+ @playground_router.get("/workflows/{workflow_id}/sessions/{session_id}")
693
759
  async def get_workflow_session(
694
760
  workflow_id: str, session_id: str, user_id: Optional[str] = Query(None, min_length=1)
695
761
  ):
@@ -704,15 +770,18 @@ def get_async_playground_router(
704
770
 
705
771
  # Retrieve the specific session
706
772
  try:
707
- workflow_session: Optional[WorkflowSession] = workflow.storage.read(session_id, user_id) # type: ignore
773
+ workflow_session = workflow.storage.read(session_id, user_id) # type: ignore
708
774
  except Exception as e:
709
775
  raise HTTPException(status_code=500, detail=f"Error retrieving session: {str(e)}")
710
776
 
711
777
  if not workflow_session:
712
778
  raise HTTPException(status_code=404, detail="Session not found")
713
779
 
714
- # Return the session
715
- return workflow_session
780
+ workflow_session_dict = workflow_session.to_dict()
781
+ if "memory" not in workflow_session_dict:
782
+ workflow_session_dict["memory"] = {"runs": workflow_session_dict.pop("runs", [])}
783
+
784
+ return JSONResponse(content=workflow_session_dict)
716
785
 
717
786
  @playground_router.post("/workflows/{workflow_id}/sessions/{session_id}/rename")
718
787
  async def rename_workflow_session(workflow_id: str, session_id: str, body: WorkflowRenameRequest):
@@ -92,30 +92,36 @@ def get_session_title_from_workflow_session(workflow_session: WorkflowSession) -
92
92
  )
93
93
  if session_name is not None:
94
94
  return session_name
95
- memory = workflow_session.memory
96
- if memory is not None:
97
- runs = memory.get("runs")
98
- runs = cast(List[Any], runs)
99
- for _run in runs:
100
- try:
101
- # Try to get content directly from the run first (workflow structure)
102
- content = _run.get("content")
103
- if content:
104
- # Split content by newlines and take first line, but limit to 100 chars
105
- first_line = content.split("\n")[0]
106
- return first_line[:100] + "..." if len(first_line) > 100 else first_line
107
-
108
- # Fallback to response.content structure (if it exists)
109
- response = _run.get("response")
110
- if response:
111
- content = response.get("content")
95
+ if hasattr(workflow_session, "memory"):
96
+ memory = workflow_session.memory
97
+ if memory is not None:
98
+ runs = memory.get("runs")
99
+ runs = cast(List[Any], runs)
100
+ for _run in runs:
101
+ try:
102
+ # Try to get content directly from the run first (workflow structure)
103
+ content = _run.get("content")
112
104
  if content:
113
105
  # Split content by newlines and take first line, but limit to 100 chars
114
106
  first_line = content.split("\n")[0]
115
107
  return first_line[:100] + "..." if len(first_line) > 100 else first_line
116
108
 
117
- except Exception as e:
118
- logger.error(f"Error parsing workflow session: {e}")
109
+ # Fallback to response.content structure (if it exists)
110
+ response = _run.get("response")
111
+ if response:
112
+ content = response.get("content")
113
+ if content:
114
+ # Split content by newlines and take first line, but limit to 100 chars
115
+ first_line = content.split("\n")[0]
116
+ return first_line[:100] + "..." if len(first_line) > 100 else first_line
117
+
118
+ except Exception as e:
119
+ logger.error(f"Error parsing workflow session: {e}")
120
+ if hasattr(workflow_session, "runs"):
121
+ if workflow_session.runs is not None and len(workflow_session.runs) > 0:
122
+ for _run in workflow_session.runs:
123
+ if _run.content:
124
+ return _run.content[:100] + "..." if len(_run.content) > 100 else _run.content
119
125
  return "Unnamed session"
120
126
 
121
127
 
@@ -107,6 +107,7 @@ class WorkflowRunRequest(BaseModel):
107
107
  input: Dict[str, Any]
108
108
  user_id: Optional[str] = None
109
109
  session_id: Optional[str] = None
110
+ stream: bool = True
110
111
 
111
112
 
112
113
  class WorkflowSessionResponse(BaseModel):
@@ -38,11 +38,13 @@ from agno.memory.agent import AgentMemory
38
38
  from agno.memory.v2 import Memory
39
39
  from agno.run.response import RunResponseErrorEvent, RunResponseEvent
40
40
  from agno.run.team import RunResponseErrorEvent as TeamRunResponseErrorEvent
41
+ from agno.run.v2.workflow import WorkflowErrorEvent
41
42
  from agno.storage.session.agent import AgentSession
42
43
  from agno.storage.session.team import TeamSession
43
44
  from agno.storage.session.workflow import WorkflowSession
44
45
  from agno.team.team import Team
45
46
  from agno.utils.log import logger
47
+ from agno.workflow.v2.workflow import Workflow as WorkflowV2
46
48
  from agno.workflow.workflow import Workflow
47
49
 
48
50
 
@@ -147,6 +149,31 @@ def team_chat_response_streamer(
147
149
  return
148
150
 
149
151
 
152
+ def workflow_response_streamer(
153
+ workflow: WorkflowV2,
154
+ body: WorkflowRunRequest,
155
+ ) -> Generator:
156
+ try:
157
+ run_response = workflow.run(
158
+ **body.input,
159
+ user_id=body.user_id,
160
+ session_id=body.session_id or str(uuid4()),
161
+ stream=True,
162
+ stream_intermediate_steps=True,
163
+ )
164
+ for run_response_chunk in run_response:
165
+ yield run_response_chunk.to_json()
166
+ except Exception as e:
167
+ import traceback
168
+
169
+ traceback.print_exc(limit=3)
170
+ error_response = WorkflowErrorEvent(
171
+ error=str(e),
172
+ )
173
+ yield error_response.to_json()
174
+ return
175
+
176
+
150
177
  def get_sync_playground_router(
151
178
  agents: Optional[List[Agent]] = None,
152
179
  workflows: Optional[List[Workflow]] = None,
@@ -615,13 +642,22 @@ def get_sync_playground_router(
615
642
  if workflow is None:
616
643
  raise HTTPException(status_code=404, detail="Workflow not found")
617
644
 
618
- return WorkflowGetResponse(
619
- workflow_id=workflow.workflow_id,
620
- name=workflow.name,
621
- description=workflow.description,
622
- parameters=workflow._run_parameters or {},
623
- storage=workflow.storage.__class__.__name__ if workflow.storage else None,
624
- )
645
+ if isinstance(workflow, Workflow):
646
+ return WorkflowGetResponse(
647
+ workflow_id=workflow.workflow_id,
648
+ name=workflow.name,
649
+ description=workflow.description,
650
+ parameters=workflow._run_parameters or {},
651
+ storage=workflow.storage.__class__.__name__ if workflow.storage else None,
652
+ )
653
+ else:
654
+ return WorkflowGetResponse(
655
+ workflow_id=workflow.workflow_id,
656
+ name=workflow.name,
657
+ description=workflow.description,
658
+ parameters=workflow.run_parameters,
659
+ storage=workflow.storage.__class__.__name__ if workflow.storage else None,
660
+ )
625
661
 
626
662
  @playground_router.post("/workflows/{workflow_id}/runs")
627
663
  def create_workflow_run(workflow_id: str, body: WorkflowRunRequest):
@@ -631,24 +667,52 @@ def get_sync_playground_router(
631
667
  raise HTTPException(status_code=404, detail="Workflow not found")
632
668
 
633
669
  # Create a new instance of this workflow
634
- new_workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id})
635
- new_workflow_instance.user_id = body.user_id
636
- new_workflow_instance.session_name = None
670
+ if isinstance(workflow, Workflow):
671
+ new_workflow_instance = workflow.deep_copy(
672
+ update={"workflow_id": workflow_id, "session_id": body.session_id}
673
+ )
674
+ new_workflow_instance.user_id = body.user_id
675
+ new_workflow_instance.session_name = None
637
676
 
638
- # Return based on the response type
639
- try:
640
- if new_workflow_instance._run_return_type == "RunResponse":
641
- # Return as a normal response
642
- return new_workflow_instance.run(**body.input)
643
- else:
644
- # Return as a streaming response
645
- return StreamingResponse(
646
- (result.to_json() for result in new_workflow_instance.run(**body.input)),
647
- media_type="text/event-stream",
648
- )
649
- except Exception as e:
650
- # Handle unexpected runtime errors
651
- raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
677
+ # Return based on the response type
678
+ try:
679
+ if new_workflow_instance._run_return_type == "RunResponse":
680
+ # Return as a normal response
681
+ return new_workflow_instance.run(**body.input)
682
+ else:
683
+ # Return as a streaming response
684
+ return StreamingResponse(
685
+ (result.to_json() for result in new_workflow_instance.run(**body.input)),
686
+ media_type="text/event-stream",
687
+ headers={
688
+ "Access-Control-Allow-Origin": "*",
689
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
690
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
691
+ },
692
+ )
693
+ except Exception as e:
694
+ # Handle unexpected runtime errors
695
+ raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
696
+ else:
697
+ # Return based on the response type
698
+ try:
699
+ if body.stream:
700
+ # Return as a streaming response
701
+ return StreamingResponse(
702
+ workflow_response_streamer(workflow, body),
703
+ media_type="text/event-stream",
704
+ headers={
705
+ "Access-Control-Allow-Origin": "*",
706
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
707
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
708
+ },
709
+ )
710
+ else:
711
+ # Return as a normal response
712
+ return workflow.arun(**body.input, session_id=body.session_id or str(uuid4()), user_id=body.user_id)
713
+ except Exception as e:
714
+ # Handle unexpected runtime errors
715
+ raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
652
716
 
653
717
  @playground_router.get("/workflows/{workflow_id}/sessions")
654
718
  def get_all_workflow_sessions(workflow_id: str, user_id: Optional[str] = Query(None, min_length=1)):
@@ -703,8 +767,11 @@ def get_sync_playground_router(
703
767
  if not workflow_session:
704
768
  raise HTTPException(status_code=404, detail="Session not found")
705
769
 
706
- # Return the session
707
- return workflow_session
770
+ workflow_session_dict = workflow_session.to_dict()
771
+ if "memory" not in workflow_session_dict:
772
+ workflow_session_dict["memory"] = {"runs": workflow_session_dict.pop("runs", [])}
773
+
774
+ return JSONResponse(content=workflow_session_dict)
708
775
 
709
776
  @playground_router.post("/workflows/{workflow_id}/sessions/{session_id}/rename")
710
777
  def rename_workflow_session(
agno/knowledge/agent.py CHANGED
@@ -184,7 +184,7 @@ class AgentKnowledge(BaseModel):
184
184
  # Filter out documents which already exist in the vector db
185
185
  if skip_existing:
186
186
  log_debug("Filtering out existing documents before insertion.")
187
- documents_to_load = self.filter_existing_documents(document_list)
187
+ documents_to_load = await self.async_filter_existing_documents(document_list)
188
188
 
189
189
  if documents_to_load:
190
190
  for doc in documents_to_load:
@@ -439,6 +439,43 @@ class AgentKnowledge(BaseModel):
439
439
 
440
440
  return filtered_documents
441
441
 
442
+ async def async_filter_existing_documents(self, documents: List[Document]) -> List[Document]:
443
+ """Filter out documents that already exist in the vector database.
444
+
445
+ This helper method is used across various knowledge base implementations
446
+ to avoid inserting duplicate documents.
447
+
448
+ Args:
449
+ documents (List[Document]): List of documents to filter
450
+
451
+ Returns:
452
+ List[Document]: Filtered list of documents that don't exist in the database
453
+ """
454
+ from agno.utils.log import log_debug, log_info
455
+
456
+ if not self.vector_db:
457
+ log_debug("No vector database configured, skipping document filtering")
458
+ return documents
459
+
460
+ # Use set for O(1) lookups
461
+ seen_content = set()
462
+ original_count = len(documents)
463
+ filtered_documents = []
464
+
465
+ for doc in documents:
466
+ # Check hash and existence in DB
467
+ content_hash = doc.content # Assuming doc.content is reliable hash key
468
+ if content_hash not in seen_content and not await self.vector_db.async_doc_exists(doc):
469
+ seen_content.add(content_hash)
470
+ filtered_documents.append(doc)
471
+ else:
472
+ log_debug(f"Skipping existing document: {doc.name} (or duplicate content)")
473
+
474
+ if len(filtered_documents) < original_count:
475
+ log_info(f"Skipped {original_count - len(filtered_documents)} existing/duplicate documents.")
476
+
477
+ return filtered_documents
478
+
442
479
  def _track_metadata_structure(self, metadata: Optional[Dict[str, Any]]) -> None:
443
480
  """Track metadata structure to enable filter extraction from queries
444
481
 
@@ -655,7 +692,7 @@ class AgentKnowledge(BaseModel):
655
692
  documents_to_insert = documents
656
693
  if skip_existing:
657
694
  log_debug("Filtering out existing documents before insertion.")
658
- documents_to_insert = self.filter_existing_documents(documents)
695
+ documents_to_insert = await self.async_filter_existing_documents(documents)
659
696
 
660
697
  if documents_to_insert: # type: ignore
661
698
  log_debug(f"Inserting {len(documents_to_insert)} new documents.")
@@ -32,5 +32,5 @@ class CombinedKnowledgeBase(AgentKnowledge):
32
32
 
33
33
  for kb in self.sources:
34
34
  log_debug(f"Loading documents from {kb.__class__.__name__}")
35
- async for document in await kb.async_document_lists:
35
+ async for document in kb.async_document_lists: # type: ignore
36
36
  yield document
agno/run/base.py CHANGED
@@ -206,7 +206,9 @@ class RunResponseExtraData:
206
206
  class RunStatus(str, Enum):
207
207
  """State of the main run response"""
208
208
 
209
+ pending = "PENDING"
209
210
  running = "RUNNING"
211
+ completed = "COMPLETED"
210
212
  paused = "PAUSED"
211
213
  cancelled = "CANCELLED"
212
214
  error = "ERROR"