agno 2.2.1__py3-none-any.whl → 2.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +735 -574
- agno/culture/manager.py +22 -24
- agno/db/async_postgres/__init__.py +1 -1
- agno/db/dynamo/dynamo.py +0 -2
- agno/db/firestore/firestore.py +0 -2
- agno/db/gcs_json/gcs_json_db.py +0 -4
- agno/db/gcs_json/utils.py +0 -24
- agno/db/in_memory/in_memory_db.py +0 -3
- agno/db/json/json_db.py +4 -10
- agno/db/json/utils.py +0 -24
- agno/db/mongo/mongo.py +0 -2
- agno/db/mysql/mysql.py +0 -3
- agno/db/postgres/__init__.py +1 -1
- agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
- agno/db/postgres/postgres.py +7 -10
- agno/db/postgres/utils.py +106 -2
- agno/db/redis/redis.py +0 -2
- agno/db/singlestore/singlestore.py +0 -3
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2269 -0
- agno/db/sqlite/sqlite.py +0 -2
- agno/db/sqlite/utils.py +96 -0
- agno/db/surrealdb/surrealdb.py +0 -6
- agno/knowledge/knowledge.py +3 -3
- agno/knowledge/reader/reader_factory.py +16 -0
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/memory/manager.py +28 -25
- agno/models/anthropic/claude.py +63 -6
- agno/models/base.py +251 -32
- agno/models/response.py +69 -0
- agno/os/router.py +7 -5
- agno/os/routers/memory/memory.py +2 -1
- agno/os/routers/memory/schemas.py +5 -2
- agno/os/schema.py +26 -20
- agno/os/utils.py +9 -2
- agno/run/agent.py +23 -30
- agno/run/base.py +17 -1
- agno/run/team.py +23 -29
- agno/run/workflow.py +17 -12
- agno/session/agent.py +3 -0
- agno/session/summary.py +4 -1
- agno/session/team.py +1 -1
- agno/team/team.py +591 -365
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/function.py +40 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +324 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/slack.py +18 -3
- agno/tools/tavily.py +146 -0
- agno/utils/agent.py +366 -1
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +166 -1
- agno/utils/print_response/workflow.py +17 -1
- agno/utils/team.py +89 -1
- agno/workflow/step.py +0 -1
- agno/workflow/types.py +10 -15
- {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/METADATA +28 -25
- {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/RECORD +64 -61
- agno/db/async_postgres/schemas.py +0 -139
- agno/db/async_postgres/utils.py +0 -347
- agno/tools/mcp.py +0 -679
- {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/WHEEL +0 -0
- {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/top_level.txt +0 -0
agno/utils/agent.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from asyncio import Future, Task
|
|
2
|
-
from typing import AsyncIterator, Iterator, List, Optional, Sequence, Union
|
|
2
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List, Optional, Sequence, Union
|
|
3
3
|
|
|
4
4
|
from agno.media import Audio, File, Image, Video
|
|
5
5
|
from agno.models.message import Message
|
|
6
|
+
from agno.models.metrics import Metrics
|
|
6
7
|
from agno.run.agent import RunEvent, RunInput, RunOutput, RunOutputEvent
|
|
7
8
|
from agno.run.team import RunOutputEvent as TeamRunOutputEvent
|
|
8
9
|
from agno.run.team import TeamRunOutput
|
|
@@ -16,6 +17,10 @@ from agno.utils.events import (
|
|
|
16
17
|
)
|
|
17
18
|
from agno.utils.log import log_debug, log_warning
|
|
18
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from agno.agent.agent import Agent
|
|
22
|
+
from agno.team.team import Team
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
async def await_for_background_tasks(
|
|
21
26
|
memory_task: Optional[Task] = None,
|
|
@@ -370,3 +375,363 @@ def scrub_history_messages_from_run_output(run_response: Union[RunOutput, TeamRu
|
|
|
370
375
|
# Remove messages with from_history=True
|
|
371
376
|
if run_response.messages:
|
|
372
377
|
run_response.messages = [msg for msg in run_response.messages if not msg.from_history]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_run_output_util(
|
|
381
|
+
entity: Union["Agent", "Team"], run_id: str, session_id: Optional[str] = None
|
|
382
|
+
) -> Optional[Union[RunOutput, TeamRunOutput]]:
|
|
383
|
+
"""
|
|
384
|
+
Get a RunOutput from the database.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
run_id (str): The run_id to load from storage.
|
|
388
|
+
session_id (Optional[str]): The session_id to load from storage.
|
|
389
|
+
"""
|
|
390
|
+
if session_id is not None:
|
|
391
|
+
if entity._has_async_db():
|
|
392
|
+
raise ValueError("Async database not supported for sync functions")
|
|
393
|
+
|
|
394
|
+
session = entity.get_session(session_id=session_id)
|
|
395
|
+
if session is not None:
|
|
396
|
+
run_response = session.get_run(run_id=run_id)
|
|
397
|
+
if run_response is not None:
|
|
398
|
+
return run_response
|
|
399
|
+
else:
|
|
400
|
+
log_warning(f"RunOutput {run_id} not found in Session {session_id}")
|
|
401
|
+
elif entity.cached_session is not None:
|
|
402
|
+
run_response = entity.cached_session.get_run(run_id=run_id)
|
|
403
|
+
if run_response is not None:
|
|
404
|
+
return run_response
|
|
405
|
+
else:
|
|
406
|
+
log_warning(f"RunOutput {run_id} not found in Session {entity.cached_session.session_id}")
|
|
407
|
+
return None
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
async def aget_run_output_util(
|
|
412
|
+
entity: Union["Agent", "Team"], run_id: str, session_id: Optional[str] = None
|
|
413
|
+
) -> Optional[Union[RunOutput, TeamRunOutput]]:
|
|
414
|
+
"""
|
|
415
|
+
Get a RunOutput from the database.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
run_id (str): The run_id to load from storage.
|
|
419
|
+
session_id (Optional[str]): The session_id to load from storage.
|
|
420
|
+
"""
|
|
421
|
+
if session_id is not None:
|
|
422
|
+
session = await entity.aget_session(session_id=session_id)
|
|
423
|
+
if session is not None:
|
|
424
|
+
run_response = session.get_run(run_id=run_id)
|
|
425
|
+
if run_response is not None:
|
|
426
|
+
return run_response
|
|
427
|
+
else:
|
|
428
|
+
log_warning(f"RunOutput {run_id} not found in Session {session_id}")
|
|
429
|
+
elif entity.cached_session is not None:
|
|
430
|
+
run_response = entity.cached_session.get_run(run_id=run_id)
|
|
431
|
+
if run_response is not None:
|
|
432
|
+
return run_response
|
|
433
|
+
else:
|
|
434
|
+
log_warning(f"RunOutput {run_id} not found in Session {entity.cached_session.session_id}")
|
|
435
|
+
return None
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def get_last_run_output_util(
|
|
440
|
+
entity: Union["Agent", "Team"], session_id: Optional[str] = None
|
|
441
|
+
) -> Optional[Union[RunOutput, TeamRunOutput]]:
|
|
442
|
+
"""
|
|
443
|
+
Get the last run response from the database.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
session_id (Optional[str]): The session_id to load from storage.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
RunOutput: The last run response from the database.
|
|
450
|
+
"""
|
|
451
|
+
if session_id is not None:
|
|
452
|
+
if entity._has_async_db():
|
|
453
|
+
raise ValueError("Async database not supported for sync functions")
|
|
454
|
+
|
|
455
|
+
session = entity.get_session(session_id=session_id)
|
|
456
|
+
if session is not None and session.runs is not None and len(session.runs) > 0:
|
|
457
|
+
for run_output in reversed(session.runs):
|
|
458
|
+
if entity.__class__.__name__ == "Agent":
|
|
459
|
+
if hasattr(run_output, "agent_id") and run_output.agent_id == entity.id:
|
|
460
|
+
return run_output
|
|
461
|
+
elif entity.__class__.__name__ == "Team":
|
|
462
|
+
if hasattr(run_output, "team_id") and run_output.team_id == entity.id:
|
|
463
|
+
return run_output
|
|
464
|
+
else:
|
|
465
|
+
log_warning(f"No run responses found in Session {session_id}")
|
|
466
|
+
|
|
467
|
+
elif (
|
|
468
|
+
entity.cached_session is not None
|
|
469
|
+
and entity.cached_session.runs is not None
|
|
470
|
+
and len(entity.cached_session.runs) > 0
|
|
471
|
+
):
|
|
472
|
+
for run_output in reversed(entity.cached_session.runs):
|
|
473
|
+
if entity.__class__.__name__ == "Agent":
|
|
474
|
+
if hasattr(run_output, "agent_id") and run_output.agent_id == entity.id:
|
|
475
|
+
return run_output
|
|
476
|
+
elif entity.__class__.__name__ == "Team":
|
|
477
|
+
if hasattr(run_output, "team_id") and run_output.team_id == entity.id:
|
|
478
|
+
return run_output
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
async def aget_last_run_output_util(
|
|
483
|
+
entity: Union["Agent", "Team"], session_id: Optional[str] = None
|
|
484
|
+
) -> Optional[Union[RunOutput, TeamRunOutput]]:
|
|
485
|
+
"""
|
|
486
|
+
Get the last run response from the database.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
session_id (Optional[str]): The session_id to load from storage.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
RunOutput: The last run response from the database.
|
|
493
|
+
"""
|
|
494
|
+
if session_id is not None:
|
|
495
|
+
session = await entity.aget_session(session_id=session_id)
|
|
496
|
+
if session is not None and session.runs is not None and len(session.runs) > 0:
|
|
497
|
+
for run_output in reversed(session.runs):
|
|
498
|
+
if entity.__class__.__name__ == "Agent":
|
|
499
|
+
if hasattr(run_output, "agent_id") and run_output.agent_id == entity.id:
|
|
500
|
+
return run_output
|
|
501
|
+
elif entity.__class__.__name__ == "Team":
|
|
502
|
+
if hasattr(run_output, "team_id") and run_output.team_id == entity.id:
|
|
503
|
+
return run_output
|
|
504
|
+
else:
|
|
505
|
+
log_warning(f"No run responses found in Session {session_id}")
|
|
506
|
+
|
|
507
|
+
elif (
|
|
508
|
+
entity.cached_session is not None
|
|
509
|
+
and entity.cached_session.runs is not None
|
|
510
|
+
and len(entity.cached_session.runs) > 0
|
|
511
|
+
):
|
|
512
|
+
for run_output in reversed(entity.cached_session.runs):
|
|
513
|
+
if entity.__class__.__name__ == "Agent":
|
|
514
|
+
if hasattr(run_output, "agent_id") and run_output.agent_id == entity.id:
|
|
515
|
+
return run_output
|
|
516
|
+
elif entity.__class__.__name__ == "Team":
|
|
517
|
+
if hasattr(run_output, "team_id") and run_output.team_id == entity.id:
|
|
518
|
+
return run_output
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def set_session_name_util(
|
|
523
|
+
entity: Union["Agent", "Team"], session_id: str, autogenerate: bool = False, session_name: Optional[str] = None
|
|
524
|
+
) -> Union[AgentSession, TeamSession]:
|
|
525
|
+
"""Set the session name and save to storage"""
|
|
526
|
+
if entity._has_async_db():
|
|
527
|
+
raise ValueError("Async database not supported for sync functions")
|
|
528
|
+
|
|
529
|
+
session = entity.get_session(session_id=session_id) # type: ignore
|
|
530
|
+
|
|
531
|
+
if session is None:
|
|
532
|
+
raise Exception("No session found")
|
|
533
|
+
|
|
534
|
+
# -*- Generate name for session
|
|
535
|
+
if autogenerate:
|
|
536
|
+
session_name = entity.generate_session_name(session=session) # type: ignore
|
|
537
|
+
log_debug(f"Generated Session Name: {session_name}")
|
|
538
|
+
elif session_name is None:
|
|
539
|
+
raise Exception("No session name provided")
|
|
540
|
+
|
|
541
|
+
# -*- Rename session
|
|
542
|
+
if session.session_data is None:
|
|
543
|
+
session.session_data = {"session_name": session_name}
|
|
544
|
+
else:
|
|
545
|
+
session.session_data["session_name"] = session_name
|
|
546
|
+
# -*- Save to storage
|
|
547
|
+
entity.save_session(session=session) # type: ignore
|
|
548
|
+
|
|
549
|
+
return session
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
async def aset_session_name_util(
|
|
553
|
+
entity: Union["Agent", "Team"], session_id: str, autogenerate: bool = False, session_name: Optional[str] = None
|
|
554
|
+
) -> Union[AgentSession, TeamSession]:
|
|
555
|
+
"""Set the session name and save to storage"""
|
|
556
|
+
session = await entity.aget_session(session_id=session_id) # type: ignore
|
|
557
|
+
|
|
558
|
+
if session is None:
|
|
559
|
+
raise Exception("Session not found")
|
|
560
|
+
|
|
561
|
+
# -*- Generate name for session
|
|
562
|
+
if autogenerate:
|
|
563
|
+
session_name = entity.generate_session_name(session=session) # type: ignore
|
|
564
|
+
log_debug(f"Generated Session Name: {session_name}")
|
|
565
|
+
elif session_name is None:
|
|
566
|
+
raise Exception("No session name provided")
|
|
567
|
+
|
|
568
|
+
# -*- Rename session
|
|
569
|
+
if session.session_data is None:
|
|
570
|
+
session.session_data = {"session_name": session_name}
|
|
571
|
+
else:
|
|
572
|
+
session.session_data["session_name"] = session_name
|
|
573
|
+
|
|
574
|
+
# -*- Save to storage
|
|
575
|
+
await entity.asave_session(session=session) # type: ignore
|
|
576
|
+
|
|
577
|
+
return session
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def get_session_name_util(entity: Union["Agent", "Team"], session_id: str) -> str:
|
|
581
|
+
"""Get the session name for the given session ID and user ID."""
|
|
582
|
+
|
|
583
|
+
if entity._has_async_db():
|
|
584
|
+
raise ValueError("Async database not supported for sync functions")
|
|
585
|
+
|
|
586
|
+
session = entity.get_session(session_id=session_id) # type: ignore
|
|
587
|
+
if session is None:
|
|
588
|
+
raise Exception("Session not found")
|
|
589
|
+
return session.session_data.get("session_name", "") if session.session_data is not None else "" # type: ignore
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
async def aget_session_name_util(entity: Union["Agent", "Team"], session_id: str) -> str:
|
|
593
|
+
"""Get the session name for the given session ID and user ID."""
|
|
594
|
+
session = await entity.aget_session(session_id=session_id) # type: ignore
|
|
595
|
+
if session is None:
|
|
596
|
+
raise Exception("Session not found")
|
|
597
|
+
return session.session_data.get("session_name", "") if session.session_data is not None else "" # type: ignore
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def get_session_state_util(entity: Union["Agent", "Team"], session_id: str) -> Dict[str, Any]:
|
|
601
|
+
"""Get the session state for the given session ID and user ID."""
|
|
602
|
+
if entity._has_async_db():
|
|
603
|
+
raise ValueError("Async database not supported for sync functions")
|
|
604
|
+
|
|
605
|
+
session = entity.get_session(session_id=session_id) # type: ignore
|
|
606
|
+
if session is None:
|
|
607
|
+
raise Exception("Session not found")
|
|
608
|
+
return session.session_data.get("session_state", {}) if session.session_data is not None else {} # type: ignore
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
async def aget_session_state_util(entity: Union["Agent", "Team"], session_id: str) -> Dict[str, Any]:
|
|
612
|
+
"""Get the session state for the given session ID and user ID."""
|
|
613
|
+
session = await entity.aget_session(session_id=session_id) # type: ignore
|
|
614
|
+
if session is None:
|
|
615
|
+
raise Exception("Session not found")
|
|
616
|
+
return session.session_data.get("session_state", {}) if session.session_data is not None else {} # type: ignore
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def update_session_state_util(
|
|
620
|
+
entity: Union["Agent", "Team"], session_state_updates: Dict[str, Any], session_id: str
|
|
621
|
+
) -> str:
|
|
622
|
+
"""
|
|
623
|
+
Update the session state for the given session ID and user ID.
|
|
624
|
+
Args:
|
|
625
|
+
session_state_updates: The updates to apply to the session state. Should be a dictionary of key-value pairs.
|
|
626
|
+
session_id: The session ID to update. If not provided, the current cached session ID is used.
|
|
627
|
+
Returns:
|
|
628
|
+
dict: The updated session state.
|
|
629
|
+
"""
|
|
630
|
+
if entity._has_async_db():
|
|
631
|
+
raise ValueError("Async database not supported for sync functions")
|
|
632
|
+
|
|
633
|
+
session = entity.get_session(session_id=session_id) # type: ignore
|
|
634
|
+
if session is None:
|
|
635
|
+
raise Exception("Session not found")
|
|
636
|
+
|
|
637
|
+
if session.session_data is not None and "session_state" not in session.session_data:
|
|
638
|
+
session.session_data["session_state"] = {}
|
|
639
|
+
|
|
640
|
+
for key, value in session_state_updates.items():
|
|
641
|
+
session.session_data["session_state"][key] = value # type: ignore
|
|
642
|
+
|
|
643
|
+
entity.save_session(session=session) # type: ignore
|
|
644
|
+
|
|
645
|
+
return session.session_data["session_state"] # type: ignore
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
async def aupdate_session_state_util(
|
|
649
|
+
entity: Union["Agent", "Team"], session_state_updates: Dict[str, Any], session_id: str
|
|
650
|
+
) -> str:
|
|
651
|
+
"""
|
|
652
|
+
Update the session state for the given session ID and user ID.
|
|
653
|
+
Args:
|
|
654
|
+
session_state_updates: The updates to apply to the session state. Should be a dictionary of key-value pairs.
|
|
655
|
+
session_id: The session ID to update. If not provided, the current cached session ID is used.
|
|
656
|
+
Returns:
|
|
657
|
+
dict: The updated session state.
|
|
658
|
+
"""
|
|
659
|
+
session = await entity.aget_session(session_id=session_id) # type: ignore
|
|
660
|
+
if session is None:
|
|
661
|
+
raise Exception("Session not found")
|
|
662
|
+
|
|
663
|
+
if session.session_data is not None and "session_state" not in session.session_data:
|
|
664
|
+
session.session_data["session_state"] = {}
|
|
665
|
+
|
|
666
|
+
for key, value in session_state_updates.items():
|
|
667
|
+
session.session_data["session_state"][key] = value # type: ignore
|
|
668
|
+
|
|
669
|
+
await entity.asave_session(session=session) # type: ignore
|
|
670
|
+
|
|
671
|
+
return session.session_data["session_state"] # type: ignore
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def get_session_metrics_util(entity: Union["Agent", "Team"], session_id: str) -> Optional[Metrics]:
|
|
675
|
+
"""Get the session metrics for the given session ID and user ID."""
|
|
676
|
+
if entity._has_async_db():
|
|
677
|
+
raise ValueError("Async database not supported for sync functions")
|
|
678
|
+
|
|
679
|
+
session = entity.get_session(session_id=session_id) # type: ignore
|
|
680
|
+
if session is None:
|
|
681
|
+
raise Exception("Session not found")
|
|
682
|
+
|
|
683
|
+
if session.session_data is not None:
|
|
684
|
+
if isinstance(session.session_data.get("session_metrics"), dict):
|
|
685
|
+
return Metrics(**session.session_data.get("session_metrics", {}))
|
|
686
|
+
elif isinstance(session.session_data.get("session_metrics"), Metrics):
|
|
687
|
+
return session.session_data.get("session_metrics")
|
|
688
|
+
return None
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
async def aget_session_metrics_util(entity: Union["Agent", "Team"], session_id: str) -> Optional[Metrics]:
|
|
692
|
+
"""Get the session metrics for the given session ID and user ID."""
|
|
693
|
+
session = await entity.aget_session(session_id=session_id) # type: ignore
|
|
694
|
+
if session is None:
|
|
695
|
+
raise Exception("Session not found")
|
|
696
|
+
|
|
697
|
+
if session.session_data is not None:
|
|
698
|
+
if isinstance(session.session_data.get("session_metrics"), dict):
|
|
699
|
+
return Metrics(**session.session_data.get("session_metrics", {}))
|
|
700
|
+
elif isinstance(session.session_data.get("session_metrics"), Metrics):
|
|
701
|
+
return session.session_data.get("session_metrics")
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def get_chat_history_util(entity: Union["Agent", "Team"], session_id: str) -> List[Message]:
|
|
706
|
+
"""Read the chat history from the session
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
|
|
710
|
+
Returns:
|
|
711
|
+
List[Message]: The chat history from the session.
|
|
712
|
+
"""
|
|
713
|
+
if entity._has_async_db():
|
|
714
|
+
raise ValueError("Async database not supported for sync functions")
|
|
715
|
+
|
|
716
|
+
session = entity.get_session(session_id=session_id) # type: ignore
|
|
717
|
+
|
|
718
|
+
if session is None:
|
|
719
|
+
raise Exception("Session not found")
|
|
720
|
+
|
|
721
|
+
return session.get_chat_history()
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
async def aget_chat_history_util(entity: Union["Agent", "Team"], session_id: str) -> List[Message]:
|
|
725
|
+
"""Read the chat history from the session
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
|
|
729
|
+
Returns:
|
|
730
|
+
List[Message]: The chat history from the session.
|
|
731
|
+
"""
|
|
732
|
+
session = await entity.aget_session(session_id=session_id) # type: ignore
|
|
733
|
+
|
|
734
|
+
if session is None:
|
|
735
|
+
raise Exception("Session not found")
|
|
736
|
+
|
|
737
|
+
return session.get_chat_history()
|
agno/utils/mcp.py
CHANGED
|
@@ -27,9 +27,13 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
|
|
|
27
27
|
Returns:
|
|
28
28
|
Callable: The entrypoint function for the tool
|
|
29
29
|
"""
|
|
30
|
-
from agno.agent import Agent
|
|
31
30
|
|
|
32
|
-
async def call_tool(
|
|
31
|
+
async def call_tool(tool_name: str, **kwargs) -> ToolResult:
|
|
32
|
+
try:
|
|
33
|
+
await session.send_ping()
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(e)
|
|
36
|
+
|
|
33
37
|
try:
|
|
34
38
|
log_debug(f"Calling MCP Tool '{tool_name}' with args: {kwargs}")
|
|
35
39
|
result: CallToolResult = await session.call_tool(tool_name, kwargs) # type: ignore
|
|
@@ -122,3 +126,89 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
|
|
|
122
126
|
return ToolResult(content=f"Error: {e}")
|
|
123
127
|
|
|
124
128
|
return partial(call_tool, tool_name=tool.name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def prepare_command(command: str) -> list[str]:
|
|
132
|
+
"""Sanitize a command and split it into parts before using it to run a MCP server."""
|
|
133
|
+
import os
|
|
134
|
+
import shutil
|
|
135
|
+
from shlex import split
|
|
136
|
+
|
|
137
|
+
# Block dangerous characters
|
|
138
|
+
if any(char in command for char in ["&", "|", ";", "`", "$", "(", ")"]):
|
|
139
|
+
raise ValueError("MCP command can't contain shell metacharacters")
|
|
140
|
+
|
|
141
|
+
parts = split(command)
|
|
142
|
+
if not parts:
|
|
143
|
+
raise ValueError("MCP command can't be empty")
|
|
144
|
+
|
|
145
|
+
# Only allow specific executables
|
|
146
|
+
ALLOWED_COMMANDS = {
|
|
147
|
+
# Python
|
|
148
|
+
"python",
|
|
149
|
+
"python3",
|
|
150
|
+
"uv",
|
|
151
|
+
"uvx",
|
|
152
|
+
"pipx",
|
|
153
|
+
# Node
|
|
154
|
+
"node",
|
|
155
|
+
"npm",
|
|
156
|
+
"npx",
|
|
157
|
+
"yarn",
|
|
158
|
+
"pnpm",
|
|
159
|
+
"bun",
|
|
160
|
+
# Other runtimes
|
|
161
|
+
"deno",
|
|
162
|
+
"java",
|
|
163
|
+
"ruby",
|
|
164
|
+
"docker",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
executable = parts[0].split("/")[-1]
|
|
168
|
+
|
|
169
|
+
# Check if it's a relative path starting with ./ or ../
|
|
170
|
+
if executable.startswith("./") or executable.startswith("../"):
|
|
171
|
+
# Allow relative paths to binaries
|
|
172
|
+
return parts
|
|
173
|
+
|
|
174
|
+
# Check if it's an absolute path to a binary
|
|
175
|
+
if executable.startswith("/") and os.path.isfile(executable):
|
|
176
|
+
# Allow absolute paths to existing files
|
|
177
|
+
return parts
|
|
178
|
+
|
|
179
|
+
# Check if it's a binary in current directory without ./
|
|
180
|
+
if "/" not in executable and os.path.isfile(executable):
|
|
181
|
+
# Allow binaries in current directory
|
|
182
|
+
return parts
|
|
183
|
+
|
|
184
|
+
# Check if it's a binary in PATH
|
|
185
|
+
if shutil.which(executable):
|
|
186
|
+
return parts
|
|
187
|
+
|
|
188
|
+
if executable not in ALLOWED_COMMANDS:
|
|
189
|
+
raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
|
|
190
|
+
|
|
191
|
+
first_part = parts[0]
|
|
192
|
+
executable = first_part.split("/")[-1]
|
|
193
|
+
|
|
194
|
+
# Allow known commands
|
|
195
|
+
if executable in ALLOWED_COMMANDS:
|
|
196
|
+
return parts
|
|
197
|
+
|
|
198
|
+
# Allow relative paths to custom binaries
|
|
199
|
+
if first_part.startswith(("./", "../")):
|
|
200
|
+
return parts
|
|
201
|
+
|
|
202
|
+
# Allow absolute paths to existing files
|
|
203
|
+
if first_part.startswith("/") and os.path.isfile(first_part):
|
|
204
|
+
return parts
|
|
205
|
+
|
|
206
|
+
# Allow binaries in current directory without ./
|
|
207
|
+
if "/" not in first_part and os.path.isfile(first_part):
|
|
208
|
+
return parts
|
|
209
|
+
|
|
210
|
+
# Allow binaries in PATH
|
|
211
|
+
if shutil.which(first_part):
|
|
212
|
+
return parts
|
|
213
|
+
|
|
214
|
+
raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
|
agno/utils/media.py
CHANGED
|
@@ -2,10 +2,11 @@ import base64
|
|
|
2
2
|
import time
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, Optional
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
8
|
|
|
9
|
+
from agno.media import Audio, File, Image, Video
|
|
9
10
|
from agno.utils.log import log_info, log_warning
|
|
10
11
|
|
|
11
12
|
|
|
@@ -185,3 +186,167 @@ def download_knowledge_filters_sample_data(
|
|
|
185
186
|
)
|
|
186
187
|
file_paths.append(str(download_path))
|
|
187
188
|
return file_paths
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def reconstruct_image_from_dict(img_data):
|
|
192
|
+
"""
|
|
193
|
+
Reconstruct an Image object from dictionary data.
|
|
194
|
+
|
|
195
|
+
Handles both base64-encoded content (from database) and regular image data (url/filepath).
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
if isinstance(img_data, dict):
|
|
199
|
+
# If content is base64 string, decode it back to bytes
|
|
200
|
+
if "content" in img_data and isinstance(img_data["content"], str):
|
|
201
|
+
return Image.from_base64(
|
|
202
|
+
img_data["content"],
|
|
203
|
+
id=img_data.get("id"),
|
|
204
|
+
mime_type=img_data.get("mime_type"),
|
|
205
|
+
format=img_data.get("format"),
|
|
206
|
+
detail=img_data.get("detail"),
|
|
207
|
+
original_prompt=img_data.get("original_prompt"),
|
|
208
|
+
revised_prompt=img_data.get("revised_prompt"),
|
|
209
|
+
alt_text=img_data.get("alt_text"),
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
# Regular image (filepath/url)
|
|
213
|
+
return Image(**img_data)
|
|
214
|
+
return img_data
|
|
215
|
+
except Exception as e:
|
|
216
|
+
log_warning(f"Failed to reconstruct image from dict: {e}")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def reconstruct_video_from_dict(vid_data):
|
|
221
|
+
"""
|
|
222
|
+
Reconstruct a Video object from dictionary data.
|
|
223
|
+
|
|
224
|
+
Handles both base64-encoded content (from database) and regular video data (url/filepath).
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
if isinstance(vid_data, dict):
|
|
228
|
+
# If content is base64 string, decode it back to bytes
|
|
229
|
+
if "content" in vid_data and isinstance(vid_data["content"], str):
|
|
230
|
+
return Video.from_base64(
|
|
231
|
+
vid_data["content"],
|
|
232
|
+
id=vid_data.get("id"),
|
|
233
|
+
mime_type=vid_data.get("mime_type"),
|
|
234
|
+
format=vid_data.get("format"),
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
# Regular video (filepath/url)
|
|
238
|
+
return Video(**vid_data)
|
|
239
|
+
return vid_data
|
|
240
|
+
except Exception as e:
|
|
241
|
+
log_warning(f"Failed to reconstruct video from dict: {e}")
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def reconstruct_audio_from_dict(aud_data):
|
|
246
|
+
"""
|
|
247
|
+
Reconstruct an Audio object from dictionary data.
|
|
248
|
+
|
|
249
|
+
Handles both base64-encoded content (from database) and regular audio data (url/filepath).
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
if isinstance(aud_data, dict):
|
|
253
|
+
# If content is base64 string, decode it back to bytes
|
|
254
|
+
if "content" in aud_data and isinstance(aud_data["content"], str):
|
|
255
|
+
return Audio.from_base64(
|
|
256
|
+
aud_data["content"],
|
|
257
|
+
id=aud_data.get("id"),
|
|
258
|
+
mime_type=aud_data.get("mime_type"),
|
|
259
|
+
transcript=aud_data.get("transcript"),
|
|
260
|
+
expires_at=aud_data.get("expires_at"),
|
|
261
|
+
sample_rate=aud_data.get("sample_rate", 24000),
|
|
262
|
+
channels=aud_data.get("channels", 1),
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
# Regular audio (filepath/url)
|
|
266
|
+
return Audio(**aud_data)
|
|
267
|
+
return aud_data
|
|
268
|
+
except Exception as e:
|
|
269
|
+
log_warning(f"Failed to reconstruct audio from dict: {e}")
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def reconstruct_file_from_dict(file_data):
|
|
274
|
+
"""
|
|
275
|
+
Reconstruct a File object from dictionary data.
|
|
276
|
+
|
|
277
|
+
Handles both base64-encoded content (from database) and regular file data (url/filepath).
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
if isinstance(file_data, dict):
|
|
281
|
+
# If content is base64 string, decode it back to bytes
|
|
282
|
+
if "content" in file_data and isinstance(file_data["content"], str):
|
|
283
|
+
return File.from_base64(
|
|
284
|
+
file_data["content"],
|
|
285
|
+
id=file_data.get("id"),
|
|
286
|
+
mime_type=file_data.get("mime_type"),
|
|
287
|
+
filename=file_data.get("filename"),
|
|
288
|
+
name=file_data.get("name"),
|
|
289
|
+
format=file_data.get("format"),
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
# Regular file (filepath/url)
|
|
293
|
+
return File(**file_data)
|
|
294
|
+
return file_data
|
|
295
|
+
except Exception as e:
|
|
296
|
+
log_warning(f"Failed to reconstruct file from dict: {e}")
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def reconstruct_images(images: Optional[List[dict]]) -> Optional[List[Image]]:
|
|
301
|
+
"""Reconstruct a list of Image objects from list of dictionaries.
|
|
302
|
+
|
|
303
|
+
Failed reconstructions are skipped with a warning logged.
|
|
304
|
+
"""
|
|
305
|
+
if not images:
|
|
306
|
+
return None
|
|
307
|
+
reconstructed = [reconstruct_image_from_dict(img_data) for img_data in images]
|
|
308
|
+
valid_images = [img for img in reconstructed if img is not None]
|
|
309
|
+
return valid_images if valid_images else None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def reconstruct_videos(videos: Optional[List[dict]]) -> Optional[List[Video]]:
|
|
313
|
+
"""Reconstruct a list of Video objects from list of dictionaries.
|
|
314
|
+
|
|
315
|
+
Failed reconstructions are skipped with a warning logged.
|
|
316
|
+
"""
|
|
317
|
+
if not videos:
|
|
318
|
+
return None
|
|
319
|
+
reconstructed = [reconstruct_video_from_dict(vid_data) for vid_data in videos]
|
|
320
|
+
valid_videos = [vid for vid in reconstructed if vid is not None]
|
|
321
|
+
return valid_videos if valid_videos else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def reconstruct_audio_list(audio: Optional[List[dict]]) -> Optional[List[Audio]]:
|
|
325
|
+
"""Reconstruct a list of Audio objects from list of dictionaries.
|
|
326
|
+
|
|
327
|
+
Failed reconstructions are skipped with a warning logged.
|
|
328
|
+
"""
|
|
329
|
+
if not audio:
|
|
330
|
+
return None
|
|
331
|
+
reconstructed = [reconstruct_audio_from_dict(aud_data) for aud_data in audio]
|
|
332
|
+
valid_audio = [aud for aud in reconstructed if aud is not None]
|
|
333
|
+
return valid_audio if valid_audio else None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def reconstruct_files(files: Optional[List[dict]]) -> Optional[List[File]]:
|
|
337
|
+
"""Reconstruct a list of File objects from list of dictionaries.
|
|
338
|
+
|
|
339
|
+
Failed reconstructions are skipped with a warning logged.
|
|
340
|
+
"""
|
|
341
|
+
if not files:
|
|
342
|
+
return None
|
|
343
|
+
reconstructed = [reconstruct_file_from_dict(file_data) for file_data in files]
|
|
344
|
+
valid_files = [f for f in reconstructed if f is not None]
|
|
345
|
+
return valid_files if valid_files else None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def reconstruct_response_audio(audio: Optional[dict]) -> Optional[Audio]:
|
|
349
|
+
"""Reconstruct a single Audio object for response audio."""
|
|
350
|
+
if not audio:
|
|
351
|
+
return None
|
|
352
|
+
return reconstruct_audio_from_dict(audio)
|