From 788cbf2cbade09a2c2edc1acdafc15c5d5f98484 Mon Sep 17 00:00:00 2001 From: Moshi Wei Date: Sat, 11 Oct 2025 17:11:55 +0800 Subject: [PATCH 1/2] clearn up deprecated code --- examples/Agent_framwork/lv0_hello_world.py | 7 - examples/Agent_framwork/lv1_single_agent.py | 24 - .../Agent_framwork/lv2_fastmcp_tool_agent.py | 79 - examples/Agent_framwork/lv2_tool_agent.py | 19 - examples/Agent_framwork/lv3_memory_agent.py | 40 - .../Agent_framwork/lv4_memory_tool_agent.py | 41 - examples/Agent_framwork/lv5_team_agent.py | 60 - .../Agent_framwork/lv6_team_agent_plus.py | 133 - examples/app/coin_donation_game.py | 334 - examples/app/meme_tweet_app.py | 164 - examples/llm/README.md | 59 - examples/llm/deepseek_tool_agent.py | 27 - isek/adapter/agno_adapter.py | 21 - isek/adapter/base.py | 59 - isek/adapter/isek_adapter.py | 21 - isek/adapter/simple_adapter.py | 35 - isek/agent/__init__.py | 4 - isek/agent/base.py | 302 - isek/agent/isek_agent.py | 133 - isek/isek_center.py | 175 - isek/memory/memory.py | 172 - isek/models/__init__.py | 0 isek/models/base.py | 291 - isek/models/litellm/__init__.py | 5 - isek/models/litellm/chat.py | 190 - isek/models/openai/__init__.py | 5 - isek/models/openai/openai.py | 154 - isek/models/provider.py | 34 - isek/models/simpleModel.py | 32 - isek/node/default_registry.py | 24 - isek/node/etcd_registry.py | 150 - isek/node/isek_center_registry.py | 286 - isek/node/node_v2.py | 185 - isek/node/registry.py | 26 - isek/protocol/a2a_protocol.py | 230 - isek/protocol/protocol.py | 41 - isek/team/__init__.py | 7 - isek/team/isek_team.py | 480 - isek/team/team.py | 8602 ----------------- isek/tools/calculator.py | 35 - isek/tools/fastmcp_toolkit.py | 244 - .../finance_toolkit/get_company_base_info.py | 48 - isek/tools/toolkit.py | 126 - pyproject.toml | 33 +- scripts/delete_all_etcd_node.py | 7 - tests/etcd_registry_test.py | 26 - tests/isek_center_registry_test.py | 24 - tests/log_test.py | 51 - tests/node_test.py | 31 - tests/test_fastmcp.py | 160 - tests/test_memory.py | 195 - tests/test_openai_simple.py | 30 - tests/test_toolkit.py | 275 - 53 files changed, 10 insertions(+), 13926 deletions(-) delete mode 100644 examples/Agent_framwork/lv0_hello_world.py delete mode 100644 examples/Agent_framwork/lv1_single_agent.py delete mode 100644 examples/Agent_framwork/lv2_fastmcp_tool_agent.py delete mode 100644 examples/Agent_framwork/lv2_tool_agent.py delete mode 100644 examples/Agent_framwork/lv3_memory_agent.py delete mode 100644 examples/Agent_framwork/lv4_memory_tool_agent.py delete mode 100644 examples/Agent_framwork/lv5_team_agent.py delete mode 100644 examples/Agent_framwork/lv6_team_agent_plus.py delete mode 100644 examples/app/coin_donation_game.py delete mode 100644 examples/app/meme_tweet_app.py delete mode 100644 examples/llm/README.md delete mode 100644 examples/llm/deepseek_tool_agent.py delete mode 100644 isek/adapter/agno_adapter.py delete mode 100644 isek/adapter/base.py delete mode 100644 isek/adapter/isek_adapter.py delete mode 100644 isek/adapter/simple_adapter.py delete mode 100644 isek/agent/__init__.py delete mode 100644 isek/agent/base.py delete mode 100644 isek/agent/isek_agent.py delete mode 100644 isek/isek_center.py delete mode 100644 isek/memory/memory.py delete mode 100644 isek/models/__init__.py delete mode 100644 isek/models/base.py delete mode 100644 isek/models/litellm/__init__.py delete mode 100644 isek/models/litellm/chat.py delete mode 100644 isek/models/openai/__init__.py delete mode 100644 isek/models/openai/openai.py delete mode 100644 isek/models/provider.py delete mode 100644 isek/models/simpleModel.py delete mode 100644 isek/node/default_registry.py delete mode 100644 isek/node/etcd_registry.py delete mode 100644 isek/node/isek_center_registry.py delete mode 100644 isek/node/node_v2.py delete mode 100644 isek/node/registry.py delete mode 100644 isek/protocol/a2a_protocol.py delete mode 100644 isek/protocol/protocol.py delete mode 100644 isek/team/__init__.py delete mode 100644 isek/team/isek_team.py delete mode 100644 isek/team/team.py delete mode 100644 isek/tools/calculator.py delete mode 100644 isek/tools/fastmcp_toolkit.py delete mode 100644 isek/tools/finance_toolkit/get_company_base_info.py delete mode 100644 isek/tools/toolkit.py delete mode 100644 scripts/delete_all_etcd_node.py delete mode 100644 tests/etcd_registry_test.py delete mode 100644 tests/isek_center_registry_test.py delete mode 100644 tests/log_test.py delete mode 100644 tests/node_test.py delete mode 100644 tests/test_fastmcp.py delete mode 100644 tests/test_memory.py delete mode 100644 tests/test_openai_simple.py delete mode 100644 tests/test_toolkit.py diff --git a/examples/Agent_framwork/lv0_hello_world.py b/examples/Agent_framwork/lv0_hello_world.py deleted file mode 100644 index 2775b69..0000000 --- a/examples/Agent_framwork/lv0_hello_world.py +++ /dev/null @@ -1,7 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai.openai import OpenAIModel -import dotenv -dotenv.load_dotenv() - -agent = IsekAgent(name='MyAgent', model=OpenAIModel()) -agent.print_response('Hello, world!') \ No newline at end of file diff --git a/examples/Agent_framwork/lv1_single_agent.py b/examples/Agent_framwork/lv1_single_agent.py deleted file mode 100644 index 0720347..0000000 --- a/examples/Agent_framwork/lv1_single_agent.py +++ /dev/null @@ -1,24 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai.openai import OpenAIModel -from isek.utils.log import LoggerManager -from isek.models.base import SimpleMessage -import dotenv -dotenv.load_dotenv() -LoggerManager.plain_mode() - - -agent = IsekAgent( - name="Email Polisher", - model=OpenAIModel(), - description="A specialized agent that helps polish and improve email content. I can help with grammar, tone, clarity, professionalism, and overall effectiveness of email communications. I provide suggestions for better structure, word choice, and formatting to make emails more impactful and professional.", -) - -# Test email to polish -test_email = """ -I wanted to reach out to my boss about the project we discussed last week. I think we should meet to talk about the next steps and see what we can do to move forward. -""" - -# response = agent.run(f"Please polish this email to make it more professional and effective:\n\n{test_email}") -# response = agent.run(test_email) -# print(response) -response = agent.print_response(test_email) diff --git a/examples/Agent_framwork/lv2_fastmcp_tool_agent.py b/examples/Agent_framwork/lv2_fastmcp_tool_agent.py deleted file mode 100644 index 9fba1da..0000000 --- a/examples/Agent_framwork/lv2_fastmcp_tool_agent.py +++ /dev/null @@ -1,79 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai import OpenAIModel -from isek.tools.fastmcp_toolkit import FastMCPToolkit -import dotenv -import os - -dotenv.load_dotenv() - -def main(): - """Example agent using FastMCP toolkit""" - - # Get GitHub token - github_token = os.getenv("GITHUB_TOKEN") - - if not github_token: - print("⚠️ Warning: GITHUB_TOKEN not set") - print("Please set GITHUB_TOKEN environment variable for GitHub Copilot MCP") - return - else: - print("✅ Using FastMCP toolkit with GitHub Copilot") - try: - # Use the pre-created fastmcp_tools instance - mcp_tools = FastMCPToolkit( - server_source="https://api.githubcopilot.com/mcp/", - name="github_fastmcp", - auth_token=github_token, - debug=True, - ) - - # Check connection status - if mcp_tools.health_check(): - print("✅ FastMCP connection successful") - available_tools = mcp_tools.list_available_tools() - print(f"Found {len(available_tools)} MCP tools") - else: - print("❌ FastMCP connection failed") - return - except Exception as e: - print(f"❌ FastMCP toolkit creation failed: {e}") - return - - print("=== Available tools ===") - tools = mcp_tools.list_available_tools() - for tool in tools: - print(f"- {tool}") - - # Create Agent - agent = IsekAgent( - name="FastMCP Assistant", - model=OpenAIModel(model_id="gpt-4o-mini"), - tools=[mcp_tools], - description="An intelligent assistant with FastMCP tool access", - instructions=[ - "Be helpful and informative", - "Use FastMCP tools when appropriate", - "Provide accurate information", - "Explain concepts clearly" - ], - success_criteria="User gets helpful responses with appropriate tool usage", - debug_mode=True - ) - - print("=== FastMCP Assistant Ready ===") - - # Test conversations - test_queries = [ - "Hello! What can you help me with?", - "Can you search for Python machine learning repositories?" - ] - - for query in test_queries: - # print(f"\n🤖 User: {query}") - # response = agent.run(query) - # print(f"🤖 Assistant: {response}") - # print("-" * 50) - agent.print_response(query) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/Agent_framwork/lv2_tool_agent.py b/examples/Agent_framwork/lv2_tool_agent.py deleted file mode 100644 index 6a45fb8..0000000 --- a/examples/Agent_framwork/lv2_tool_agent.py +++ /dev/null @@ -1,19 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai import OpenAIModel -from isek.models.base import SimpleMessage -from isek.tools.calculator import calculator_tools -import dotenv -dotenv.load_dotenv() - - -agent = IsekAgent( - name="My Agent", - model=OpenAIModel(), - tools=[calculator_tools], - description="A helpful assistant with calculator abilities", - instructions=["Be polite", "Provide accurate information", "Use tools for math questions when possible"], - success_criteria="User gets a helpful response, including math answers when needed", - debug_mode=True -) - -agent.print_response("What is 7 times 8 plus 2?") diff --git a/examples/Agent_framwork/lv3_memory_agent.py b/examples/Agent_framwork/lv3_memory_agent.py deleted file mode 100644 index af4e88a..0000000 --- a/examples/Agent_framwork/lv3_memory_agent.py +++ /dev/null @@ -1,40 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai import OpenAIModel -from isek.memory.memory import Memory -from isek.models.base import SimpleMessage -from isek.utils.print_utils import print_panel -import dotenv -dotenv.load_dotenv() - -# Create memory instance -memory = Memory(debug_mode=True) - -# Create agent with memory -agent = IsekAgent( - name="Memory Agent", - model=OpenAIModel(), - memory=memory, - description="A helpful assistant with memory", - instructions=["Be polite", "Provide accurate information", "Remember previous conversations"], - success_criteria="User gets a helpful response that takes into account previous interactions", - debug_mode=True -) - -# # Test conversation with memory - -agent.print_response("Hello! My name is Alice.", user_id="alice", session_id="session1") -agent.print_response("What's my name?", user_id="alice", session_id="session1") -agent.print_response("Tell me about our previous conversations.", user_id="alice", session_id="session1") - -# Show memory contents -total_memories = len(memory.get_user_memories('alice')) -print_panel(title=f"Memory Contents", content="", title_align="left") -for i, memory_item in enumerate(memory.get_user_memories('alice')): - print_panel(title=f"Memory {i+1}/{total_memories}", content=str(memory_item.memory), title_align="left") - -total_runs = len(memory.get_runs('session1')) -print_panel(title=f"Session runs", content="", title_align="left") -for i, run in enumerate(memory.get_runs('session1')): - print_panel(title=f"Run {i + 1}/{total_runs}", content=f"{run}", title_align="left") - - diff --git a/examples/Agent_framwork/lv4_memory_tool_agent.py b/examples/Agent_framwork/lv4_memory_tool_agent.py deleted file mode 100644 index 6ca57bf..0000000 --- a/examples/Agent_framwork/lv4_memory_tool_agent.py +++ /dev/null @@ -1,41 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai import OpenAIModel -from isek.memory.memory import Memory -from isek.tools.calculator import calculator_tools -from isek.models.base import SimpleMessage -from isek.utils.print_utils import print_panel -import dotenv -dotenv.load_dotenv() - -# Create memory instance -memory = Memory(debug_mode=True) - -# Create agent with memory and tools -agent = IsekAgent( - name="Memory Tool Agent", - model=OpenAIModel(), - memory=memory, - tools=[calculator_tools], - description="A helpful assistant with memory and calculator abilities", - instructions=["Be polite", "Provide accurate information", "Remember previous conversations", "Use tools for math questions when possible"], - success_criteria="User gets a helpful response that takes into account previous interactions and includes math answers when needed", - debug_mode=True -) - -# Test conversation with memory -agent.print_response("Hello! My name is Alice.", user_id="alice", session_id="session1") -agent.print_response("What's my name?", user_id="alice", session_id="session1") -agent.print_response("What is 5 times 6? And what's my name?", user_id="alice", session_id="session1") -agent.print_response("Tell me about our previous conversations and calculate 10 + 15.", user_id="alice", session_id="session1") - - -# Show memory contents -total_memories = len(memory.get_user_memories('alice')) -print_panel(title=f"Memory Contents", content="", title_align="left") -for i, memory_item in enumerate(memory.get_user_memories('alice')): - print_panel(title=f"Memory {i+1}/{total_memories}", content=str(memory_item.memory), title_align="left") - -total_runs = len(memory.get_runs('session1')) -print_panel(title=f"Session runs", content="", title_align="left") -for i, run in enumerate(memory.get_runs('session1')): - print_panel(title=f"Run {i + 1}/{total_runs}", content=f"{run}", title_align="left") \ No newline at end of file diff --git a/examples/Agent_framwork/lv5_team_agent.py b/examples/Agent_framwork/lv5_team_agent.py deleted file mode 100644 index c971fff..0000000 --- a/examples/Agent_framwork/lv5_team_agent.py +++ /dev/null @@ -1,60 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai.openai import OpenAIModel -from isek.memory.memory import Memory -from isek.tools.calculator import calculator_tools -from isek.tools.toolkit import Toolkit -from isek.team.isek_team import IsekTeam -import dotenv -import os - -dotenv.load_dotenv() - -# Set up models -researcher_model = OpenAIModel() -writer_model = OpenAIModel() -team_model = OpenAIModel() - -# Set up memory for each agent -researcher_memory = Memory() -writer_memory = Memory() - -# Set up toolkits -researcher_toolkit = calculator_tools -writer_toolkit = Toolkit(tools=[]) - -# Create team members (agents) -agent1 = IsekAgent( - name="Researcher", - description="Expert in research and analysis. Can use tools to calculate or retrieve data.", - model=researcher_model, - memory=researcher_memory, - tools=[researcher_toolkit] -) -agent2 = IsekAgent( - name="Writer", - description="Expert in writing and communication. Summarizes and presents information clearly.", - model=writer_model, - memory=writer_memory, - tools=[writer_toolkit] -) - -# Create the team -team = IsekTeam( - name="AI Research Team", - members=[agent1, agent2], - model=team_model, - mode="coordinate", # Try also "route" or "collaborate" - description="A team that researches the latest AI developments and writes comprehensive reports." -) - -# Run the team on a complex task -task = ( - "Research the latest advancements in AI, " - "calculate the average number of new AI papers published per month in 2023, " - "and write a concise summary for a general audience." -) -team.print_response(task) - - -# response = team.run(task) -# print("Team Response:\n", response) diff --git a/examples/Agent_framwork/lv6_team_agent_plus.py b/examples/Agent_framwork/lv6_team_agent_plus.py deleted file mode 100644 index 6b496bf..0000000 --- a/examples/Agent_framwork/lv6_team_agent_plus.py +++ /dev/null @@ -1,133 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.openai.openai import OpenAIModel -from isek.memory.memory import Memory -from isek.tools.calculator import calculator_tools -from isek.tools.toolkit import Toolkit -from isek.team.isek_team import IsekTeam -from isek.utils.print_utils import print_panel -import dotenv -import os - -dotenv.load_dotenv() - -def test_team_modes(): - """Test different team modes to demonstrate their capabilities.""" - print_panel(title="Testing Team Modes (Coordination Model)", color="yellow") - - # Set up models - researcher_model = OpenAIModel() - writer_model = OpenAIModel() - team_model = OpenAIModel() - - # Set up memory for each agent - researcher_memory = Memory() - writer_memory = Memory() - - # Set up toolkits - researcher_toolkit = calculator_tools # calculator_tools is already a Toolkit - writer_toolkit = Toolkit(tools=[]) # Writer has no tools in this example - - # Create team members (agents) - agent1 = IsekAgent( - name="Researcher", - description="Expert in research and analysis. Can use tools to calculate or retrieve data.", - model=researcher_model, - memory=researcher_memory, - tools=[researcher_toolkit] - ) - agent2 = IsekAgent( - name="Writer", - description="Expert in writing and communication. Summarizes and presents information clearly.", - model=writer_model, - memory=writer_memory, - tools=[writer_toolkit] - ) - - # Test different team modes - modes = ["coordinate", "route", "collaborate"] - - for mode in modes: - # Run the team on a task - task = ( - "Calculate 15 * 23 and then write a brief explanation of what this calculation represents." - ) - print_panel(title=f"Testing Team Mode: {mode.upper()}", content=f"Task: {task}", color="bright_blue") - # print(f"\n{'='*60}") - # print(f"Testing Team Mode: {mode.upper()}") - # print(f"{'='*60}") - - # Create the team with current mode - team = IsekTeam( - name=f"AI Research Team ({mode})", - members=[agent1, agent2], - model=team_model, - mode=mode, - description="A team that researches the latest AI developments and writes comprehensive reports.", - debug_mode=True # Enable debug to see what's happening - ) - - response = team.run(task) - print_panel(title="Response", content=response) - # print(f"Response:\n{response}") - # print("-" * 40) - -def test_simple_team(): - """Test a simple team without coordination model.""" - - print_panel(title="Testing Simple Team (No Coordination Model)", color="yellow") - - # Set up models - researcher_model = OpenAIModel() - writer_model = OpenAIModel() - team_model = OpenAIModel() - - # Set up memory for each agent - researcher_memory = Memory() - writer_memory = Memory() - - # Set up toolkits - researcher_toolkit = calculator_tools - writer_toolkit = Toolkit(tools=[]) - - # Create team members (agents) - agent1 = IsekAgent( - name="Researcher", - description="Expert in research and analysis. Can use tools to calculate or retrieve data.", - model=researcher_model, - memory=researcher_memory, - tools=[researcher_toolkit] - ) - agent2 = IsekAgent( - name="Writer", - description="Expert in writing and communication. Summarizes and presents information clearly.", - model=writer_model, - memory=writer_memory, - tools=[writer_toolkit] - ) - - # Create the team without a coordination model - team = IsekTeam( - name="Simple Research Team", - members=[agent1, agent2], - model=team_model, - mode="coordinate", # Will use simple coordination since no model - description="A simple team that researches and writes reports.", - debug_mode=True - ) - - # Run the team on a task - task = "Calculate 10 + 5 and explain what this means." - - team.print_response(task) - - -if __name__ == "__main__": - - print_panel(title="ISEK Team Agent Demo", content="Testing different team modes and configurations...", color="bright_yellow") - - # Test simple team first - test_simple_team() - - # Test different team modes - test_team_modes() - print_panel(title="ISEK Team Agent Demo", content="Demo completed!", color="bright_yellow") diff --git a/examples/app/coin_donation_game.py b/examples/app/coin_donation_game.py deleted file mode 100644 index f4c5b92..0000000 --- a/examples/app/coin_donation_game.py +++ /dev/null @@ -1,334 +0,0 @@ -import time -import os - -from isek.models.openai.openai import OpenAIModel -from isek.agent.isek_agent import IsekAgent -from dotenv import load_dotenv - - -load_dotenv() - -model = OpenAIModel( - model_id=os.environ.get("OPENAI_MODEL_NAME"), - base_url=os.environ.get("OPENAI_BASE_URL"), - api_key=os.environ.get("OPENAI_API_KEY"), -) - -P1_info = { - "name": "Doe", - "description": "An experienced game player", - "instructions": [ - "you are a player, you need to win the game", - "you are asked to donate at least one coin each round", - "if you donate the least coins, you will have to donate extra 10 coins as punishment", - "if you run out of coins, you will be lose the game", - "the last player with coins wins the game", - "you donate different amount of coins each round to confuse other players", - "output only the donation amount number without any explanation and nothing else", - ], -} - -P2_info = { - "name": "Eddie", - "description": "An experienced game player", - "instructions": [ - "you are a player, you need to win the game", - "you are asked to donate at least one coin each round", - "if you donate the least coins, you will have to donate extra 10 coins as punishment", - "if you run out of coins, you will be lose the game", - "the last player with coins wins the game", - "you are smart and able to adjust based on other players' action", - "you donate different amount of coins each round to confuse other players", - "output only the donation amount number without any explanation and nothing else", - ], -} - -P3_info = { - "name": "Bob", - "description": "An experienced game player", - "instructions": [ - "you are a player, you need to win the game", - "you are asked to donate at least one coin each round", - "if you donate the least coins, you will have to donate extra 10 coins as punishment", - "if you run out of coins, you will be lose the game", - "you always act based on other players' action", - "the last player with coins wins the game", - "you donate different amount of coins each round to confuse other players", - "output only the donation amount number without any explanation and nothing else", - ], -} - -P4_info = { - "name": "Alice", - "description": "An experienced game player", - "instructions": [ - "you are a player, you need to win the game", - "you are asked to donate at least one coin each round", - "if you donate the least coins, you will have to donate extra 10 coins as punishment", - "if you run out of coins, you will be lose the game", - "the last player with coins wins the game", - "you are super smart and you know how to win the game", - "you donate different amount of coins each round to confuse other players", - "output only the donation amount number without any explanation and nothing else", - ], -} - -info_array = [P1_info, P2_info, P3_info, P4_info] - -Agent_array = [] -for index, player in enumerate(info_array): - agent = IsekAgent( - name=player["name"], - description=player["description"], - instructions=player["instructions"], - model=model, - debug_mode=False, - ) - Agent_array.append(agent) - -time.sleep(2) - - -def print_participants_status(participants): - """Prints the status of all participants""" - print("\n--- Current Status ---") - for p in participants: - # To clearly show who is out, 0 or negative coins can be displayed - status = "Eliminated" if p["coins"] <= 0 else f"{p['coins']} coins" - print(f"{p['name']}: {status}") - print("------------------") - - -def get_player_donation(player): - """Gets valid donation input from the player""" - while True: - # If the player has no coins left, skip input directly (theoretically shouldn't happen, as they'd be filtered by active_participants) - if player["coins"] <= 0: - print(f"{player['name']} is eliminated and cannot donate.") - return 0 - - try: - donation = int( - input( - f"{player['name']} (you have {player['coins']} coins), please enter the amount of coins you want to donate (1 - {player['coins']}): " - ) - ) - if donation < 1: - print("Error: Donation amount must be at least 1.") - elif donation > player["coins"]: - print( - f"Error: You only have {player['coins']} coins, you cannot donate {donation}." - ) - else: - return donation - except ValueError: - print("Error: Please enter a valid number.") - - -def get_computer_donation(computer, donations_last_round): - # convert donations_last_round to string - donations_last_round_str = str(donations_last_round) - print(f"index {computer['index']} is thinking...") - """Gets the computer's donation""" - if computer["coins"] <= 0: - return 0 # Cannot donate anymore - # The computer donates an amount based on agent's decision - max_donation = computer["coins"] - donation_amount = Agent_array[int(computer["index"]) - 1].run( - f"you are {computer['name']}, you have {max_donation} coins, in last round, the donation status is {donations_last_round_str}, now, please, donate coins, output only the donation amount number without any explanation and nothing else" - ) - - # remove chars in donation_amount to get number only - donation_amount = "".join(filter(str.isdigit, donation_amount)) - return int(donation_amount) - - -def run_game(num_computers): - """Runs the coin donation game""" - participants = [] - start_coins = 100 - - # Initialize player - participants.append( - {"name": "Player", "index": "1", "coins": start_coins, "is_player": True} - ) - - # Initialize computers - for i in range(num_computers): - participants.append( - { - "name": f"Computer {i+1}", - "index": f"{i+1}", - "coins": start_coins, - "is_player": False, - } - ) - - round_number = 1 - donations_last_round = {} - while True: - print(f"\n=============== Round {round_number} Begins ===============") - - # --- Check if the game should end before the round starts --- - active_participants = [p for p in participants if p["coins"] > 0] - if len(active_participants) <= 1: - print("\n--- Game Over ---") - if len(active_participants) == 1: - print(f"Only {active_participants[0]['name']} remains!") - print(f"{active_participants[0]['name']} is the final winner!") - else: - # This situation can occur if two players are eliminated simultaneously in the last round - print("Everyone is eliminated! No winner.") - print_participants_status(participants) # Display final status - break # End the main game loop - - # Print status at the beginning of the round - print_participants_status(participants) - - donations_this_round = {} - # The participants_in_round list is now active_participants - participants_in_round = active_participants - - # --- Get donations (only request from active players) --- - for p in participants_in_round: - donation = 0 - if p["is_player"]: - donation = get_player_donation(p) - else: - print(f"{p['name']} is thinking...") - time.sleep(0.5) # Simulate computer thinking - donation = get_computer_donation(p, donations_last_round) - print(f"{p['name']} decides to donate {donation}") - - donations_this_round[p["name"]] = donation - time.sleep(0.5) # Simulate computer thinking - # If no one can donate this round (unlikely to happen), end the game - if not donations_this_round: - print("\nError: No one donated this round, game ended unexpectedly!") - break - - print("\n--- Round Donation Settlement ---") - donations_last_round = ( - donations_this_round.copy() - ) # Backup this round's donations - for name, donation_amount in donations_this_round.items(): - # print(f"{name} donated: {donation_amount}") # Computer's donation is already printed when obtained, player's is self-inputted - # Actually deduct the donated coins here - for p in participants: - if p["name"] == name: - p["coins"] -= donation_amount - break # If found, break the inner loop - - # --- Find the person who donated the least and penalize them --- - if donations_this_round: # Ensure someone has donated - # Find the donation amounts of those who participated in this round's donations - valid_donations = { - name: amount - for name, amount in donations_this_round.items() - if name in [p["name"] for p in participants_in_round] - } - - if valid_donations: # Ensure there are valid donors (people with coins > 0) - min_donation = min(valid_donations.values()) - losers = [ - name - for name, donation_amount in valid_donations.items() - if donation_amount == min_donation - ] - - print(f"\nMinimum donation this round: {min_donation}") - if len(losers) > 0: - print( - "Penalty! The following participants will have an additional 10 coins deducted:" - ) - for loser_name in losers: - print(f"- {loser_name}") - # Find the corresponding participant and deduct coins - for p in participants: - if p["name"] == loser_name: - p["coins"] -= 10 - break - # else: # This else should theoretically not be triggered, as there will always be a minimum value - # print("No one was penalized this round.") - else: - print("No valid donors this round for comparison.") - - # --- Check if the game should end after this round --- - active_participants_after_round = [p for p in participants if p["coins"] > 0] - if len(active_participants_after_round) <= 1: - print("\n--- Game Over ---") - # Optionally print who was eliminated this round - eliminated_this_round = [] - current_active_names = {p["name"] for p in active_participants_after_round} - for p_start in ( - participants_in_round - ): # Check players who were active at the start but are no longer active - if p_start["name"] not in current_active_names: - eliminated_this_round.append(p_start["name"]) - - if eliminated_this_round: - print( - "Players who ran out of coins this round:", - ", ".join(eliminated_this_round), - ) - - if len(active_participants_after_round) == 1: - print(f"\nOnly {active_participants_after_round[0]['name']} remains!") - print( - f"{active_participants_after_round[0]['name']} is the final winner!" - ) - else: - print("\nEveryone is eliminated! No winner.") - print_participants_status(participants) # Display final status - break # End the main game loop - - # --- Prepare for the next round --- - round_number += 1 - # time.sleep(1) # This pause can be removed or kept - - -# --- Game Start --- -if __name__ == "__main__": - print( - " ▗▖ ▗▄▖ ▗▖ ▗▖▗▄▄▄▖▗▄▄▖ ▗▄▄▖ ▗▄▖ ▗▖ ▗▖▗▄▄▄▖ ▗▖ ▗▖ ▗▄▄▄▖ ▗▄▄▖▗▄▄▄▖▗▖ ▗▖" - ) - print( - " ▐▌▐▌ ▐▌▐▌▗▞▘▐▌ ▐▌ ▐▌ ▐▌ ▐▌ ▐▌▐▛▚▞▜▌▐▌ ▝▚▞▘ █ ▐▌ ▐▌ ▐▌▗▞▘" - ) - print( - " ▐▌▐▌ ▐▌▐▛▚▖ ▐▛▀▀▘▐▛▀▚▖ ▐▌▝▜▌▐▛▀▜▌▐▌ ▐▌▐▛▀▀▘ ▐▌ █ ▝▀▚▖▐▛▀▀▘▐▛▚▖ " - ) - print( - " ▗▄▄▞▘▝▚▄▞▘▐▌ ▐▌▐▙▄▄▖▐▌ ▐▌ ▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌▐▙▄▄▖ ▗▞▘▝▚▖ ▗▄█▄▖▗▄▄▞▘▐▙▄▄▖▐▌ ▐▌" - ) - print() - - print("\nWelcome to the Coin Donation Game!") - print("=========== How to Play ===============") - print("1. In this game, each player starts with 100 coins.") - print("2. In each round, each player must donate at least one coin to the bank.") - print( - "3. The player who donates the least coins to the bank will have to donate an extra 10 coins as punishment." - ) - print("4. If a player runs out of coins, they lose the game.") - print("5. The last player with coins left wins the game.") - print("=========== Copy Right ===============") - print( - "This game is designed by Joker Game (www.thejokergame.com) and authorized to Isek to use this game as demo, all rights reserved." - ) - print() - while True: - try: - # Print("This game is ") - num_cpu = int( - input("Please enter the number of computer players (e.g., 3): ") - ) - if num_cpu >= 0: - break - else: - print("Number of computers cannot be negative.") - except ValueError: - print("Please enter a valid number.") - - run_game(num_cpu) - print("\nGame simulation finished.") diff --git a/examples/app/meme_tweet_app.py b/examples/app/meme_tweet_app.py deleted file mode 100644 index 38c5d54..0000000 --- a/examples/app/meme_tweet_app.py +++ /dev/null @@ -1,164 +0,0 @@ -from textwrap import dedent -from isek.node.node_v2 import Node -from isek.adapter.agno_adapter import AgnoAdapter -from agno.agent import Agent -from agno.models.openai import OpenAIChat -from agno.tools.duckduckgo import DuckDuckGoTools -from isek.utils.log import log - -import json -import dotenv -import os -dotenv.load_dotenv() - -def query_dexscreener(token_name): - """Query DexScreener API to get token information""" - import requests - - base_url = "https://api.dexscreener.com/latest/dex/search" - params = {"q": f"{token_name}/SOL"} - - try: - response = requests.get(base_url, params=params) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as e: - print(f"Error querying DexScreener: {e}") - return None - -# Initialize the research agent with advanced journalistic capabilities -research_agent = Agent( - model=OpenAIChat(id="gpt-4o-mini"), - tools=[DuckDuckGoTools(),query_dexscreener], - description=dedent("""\ - Yo fam! I'm your degen crypto bro who's been in the trenches since 2013 💎🙌 - Been through all the ups and downs, made and lost millions, and now I'm here to help you make it! - - What I do best: - - Spot the next 100x gem before anyone else 🚀 - - Build the strongest crypto fam on CT 🤝 - - DYOR but make it fun and profitable 📈 - - Keep it real - no BS, just pure alpha 💯\ - """), - instructions=dedent("""\ - Yo check it fam! Here's how we cook 🧑‍🍳 - - 1. Drop me the basics ⚡ - - Token name/ticker (like $PEPE, $WOJAK) - - 2. use the tool to get the token info including the following fields: - baseToken_address, - url, - baseToken_symbol, - txns, - volume, - priceChange, - liquidity, - fdv, - marketCap - - 3. Cook that Tweet 🔥 - - Catchy af headline (emojis = engagement) - - Price action that makes em FOMO - - Numbers that make sense to normies - - Drop that contract (transparency = trust) - - Hashtag game strong #IYKYK - - - Remember: We eat good together 🍽️ WAGMI\ - """), - markdown=True, - show_tool_calls=True, - add_datetime_to_instructions=True, -) - -# Conversational terminal interface -def cli_conversation(): - print("🤖 Welcome to the Agno Meme Tweet Assistant!") - print("I can help you generate a meme tweet for a given token.") - print("Type 'quit', 'exit', or 'bye' to end the conversation.") - print("-" * 50 + "\n") - - while True: - try: - # Get user input - user_input = input("\n💬 You: ").strip() - - # Check for exit commands - if user_input.lower() in ['quit', 'exit', 'bye', 'q']: - print("\n👋 Goodbye! Thanks for chatting with me!") - break - - # Skip empty input - if not user_input: - continue - - # Get agent response - print("\n🤖 Assistant: ", end="") - # response = agent.run(user_input) - - token_info = query_dexscreener(user_input) - # Get token info from DexScreener - if token_info: - print(f"\nToken Information for {user_input}:") - # print(json.dumps(token_info, indent=2)) - baseToken_address = token_info["pairs"][0]["baseToken"]["address"] - baseToken_symbol = token_info["pairs"][0]["baseToken"]["symbol"] - txns = token_info["pairs"][0]["txns"]["h24"] - volume = token_info["pairs"][0]["volume"]["h24"] - priceChange = token_info["pairs"][0]["priceChange"]["h24"] - liquidity = token_info["pairs"][0]["liquidity"]["usd"] - fdv = token_info["pairs"][0]["fdv"] - marketCap = token_info["pairs"][0]["marketCap"] - url = token_info["pairs"][0]["url"] - print(f"url: {url}") - print(f"baseToken_address: {baseToken_address}") - print(f"baseToken_symbol: {baseToken_symbol}") - print(f"txns: {txns}") - print(f"volume: {volume}") - print(f"priceChange: {priceChange}") - print(f"liquidity: {liquidity}") - print(f"fdv: {fdv}") - - user_input = f""" - token_name: {user_input} - url: {url} - baseToken_symbol: {baseToken_symbol} - txns: {txns} - volume: {volume} - priceChange: {priceChange} - liquidity: {liquidity} - fdv: {fdv} - marketCap: {marketCap} - """ - else: - print(f"\nCould not retrieve information for {user_input}") - - research_agent.print_response(user_input, markdown=True) - - except KeyboardInterrupt: - print("\n\n👋 Goodbye! Thanks for chatting with me!") - break - except Exception as e: - print(f"\n❌ Error: {e}") - print("Please try again or type 'quit' to exit.") - -if __name__ == "__main__": - - # 3. Start the Node Server with the Agent Team - server_node_id = "agent_server_1" - server_port = 9005 - print(f"Starting server node '{server_node_id}' on port {server_port} to host the agent team...") - log.info("Server node is starting up...") - - server_node = Node( - node_id=server_node_id, - port=server_port, - adapter=AgnoAdapter(agent=research_agent) - ) - - # Start the server in the foreground. It will now listen for messages. - server_node.build_server(daemon=False) - - - # cli_conversation() \ No newline at end of file diff --git a/examples/llm/README.md b/examples/llm/README.md deleted file mode 100644 index 7568403..0000000 --- a/examples/llm/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# 📈 Company Info Agent (Powered by LiteLLMModel and DeepSeek) - -This example demonstrates how to build a company information agent using the [ISEK](https://github.com/isekOS/ISEK) framework. The agent integrates the `deepseek` large language model via [`LiteLLMModel`](** https://github.com/isekOS/ISEK/tree/main/isek/models/litellm**)and can fetch stock codes and company information for companies. - ---- - -## 🚀 Features - -- Query company info by **company name** -- Retrieve **stock code** (e.g. `TSLA`) -- Fetch **basic stock info** and **company details** using tools -- Uses `DeepSeek-chat` LLM via [`LiteLLMModel`](** https://github.com/isekOS/ISEK/tree/main/isek/models/litellm**) -- Fully extensible with other models and tools via ISEK - ---- - -## 🧠 How It Works - -The agent uses the following components: - -- [`LiteLLMModel`](** https://github.com/isekOS/ISEK/tree/main/isek/models/litellm**): Wrapper for any OpenAI-compatible LLM (e.g., DeepSeek, GPT-4, Claude, etc.) -- [`base_info_tools`]: A collection of tools to retrieve stock codes, stock information, and company details. -- `IsekAgent`: Manages tool usage and reasoning based on instructions. - ---- - -## 🧩 Set Up Environment - -Before you try this example, don't forget to modify your .env file: - -```bash -DEEPSEEK_API_KEY=your_deepseek_apikay -DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 -``` - -## 🔄 Other Example Configs - -You can easily switch to other models and providers using [`LiteLLMModel`](** https://github.com/isekOS/ISEK/tree/main/isek/models/litellm**). Here are some common configurations: - -#### ✅ DeepSeek via Ollama (local deployment) - -```python -LiteLLMModel( - provider="ollama", - model_id="deepseek-chat", - base_url="http://localhost:11434", - api_env_key=None # No API key needed for local use -) -``` - -#### ✅ Claude (Anthropic) - -```python -LiteLLMModel( - provider="anthropic", - model_id="claude-3-opus-20240229", - api_env_key="ANTHROPIC_API_KEY" -) -``` \ No newline at end of file diff --git a/examples/llm/deepseek_tool_agent.py b/examples/llm/deepseek_tool_agent.py deleted file mode 100644 index 6c66d1e..0000000 --- a/examples/llm/deepseek_tool_agent.py +++ /dev/null @@ -1,27 +0,0 @@ -from isek.agent.isek_agent import IsekAgent -from isek.models.litellm import LiteLLMModel -from isek.models.base import SimpleMessage -from isek.tools.finance_toolkit.get_company_base_info import company_base_info_tools - - -import dotenv -dotenv.load_dotenv() - -agent = IsekAgent( - name="A-Share Company Info Agent", - model=LiteLLMModel(provider = "deepseek", model_id="deepseek/deepseek-chat"), - tools=[company_base_info_tools], - description="An assistant that finds stock info and company info given a company name", - instructions=["Be polite", - "Always first retrieve a numeric stock code (e.g. '600519') and a company 'About' or official website URL base on the company name.", - "Then use the appropriate tool with the stock code to retrieve basic stock information.", - "If a valid 'About' or official website URL is available, use the appropriate tool to fetch additional company details and return it.", - "Always return concise and structured information.", - "Only make tool calls when needed.", - "If a valid stock code or official company URL cannot be found, inform the user explicitly and avoid making unnecessary tool calls."], - success_criteria="User receives the correct stock info and basic company information for the given company name.", - debug_mode=True -) - -agent.print_response("hello") -agent.print_response("Give me the base info of Apple company, including its stock info and company info.") \ No newline at end of file diff --git a/isek/adapter/agno_adapter.py b/isek/adapter/agno_adapter.py deleted file mode 100644 index b08e637..0000000 --- a/isek/adapter/agno_adapter.py +++ /dev/null @@ -1,21 +0,0 @@ -from isek.adapter.base import Adapter, AdapterCard -from agno.agent import Agent - - -class AgnoAdapter(Adapter): - def __init__(self, agent: Agent): - self._agno_agent = agent - - def run(self, prompt: str, **kwargs) -> str: - """Simple response for testing.""" - return self._agno_agent.run(prompt).content - - def get_adapter_card(self) -> AdapterCard: - """Get team card for A2A protocol.""" - return AdapterCard( - name=self._agno_agent.name or "Unnamed Agent", - bio="", - lore="", - knowledge="", - routine="", - ) diff --git a/isek/adapter/base.py b/isek/adapter/base.py deleted file mode 100644 index 3c05926..0000000 --- a/isek/adapter/base.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from isek.utils.print_utils import print_response - - -@dataclass -class AdapterCard: - name: str - bio: str - lore: str - knowledge: str - routine: str - - -class Adapter(ABC): - """ - Abstract base class for all team implementations. - - This class defines the interface that all team implementations must follow. - Teams can coordinate multiple agents or other teams to work together. - """ - - @abstractmethod - def run(self, prompt: str, **kwargs) -> str: - """ - Execute the team's main functionality with the given prompt. - - Args: - prompt: The input prompt or task for the team to process - **kwargs: Additional keyword arguments - - Returns: - str: The team's response or result - - Raises: - NotImplementedError: If the concrete class doesn't implement this method - """ - pass - - @abstractmethod - def get_adapter_card(self) -> AdapterCard: - """ - Get metadata about the team for discovery and identification purposes. - - Returns: - TeamCard: A card containing team metadata including name, bio, lore, knowledge, and routine - - Raises: - NotImplementedError: If the concrete class doesn't implement this method - """ - pass - - def print_response(self, *args, **kwargs): - """ - Proxy to the shared print_response utility, passing self.run as run_func. - """ - return print_response(self.run, *args, **kwargs) diff --git a/isek/adapter/isek_adapter.py b/isek/adapter/isek_adapter.py deleted file mode 100644 index 7898b00..0000000 --- a/isek/adapter/isek_adapter.py +++ /dev/null @@ -1,21 +0,0 @@ -from isek.adapter.base import Adapter, AdapterCard -from isek.team.isek_team import IsekTeam - - -class IsekAdapter(Adapter): - def __init__(self, agent: IsekTeam): - self._isek_team = agent - - def run(self, prompt: str, **kwargs) -> str: - """Simple response for testing.""" - return self._isek_team.run(prompt) - - def get_adapter_card(self) -> AdapterCard: - """Get team card for A2A protocol.""" - return AdapterCard( - name=self._isek_team.name or "Unnamed Team", - bio="", - lore="", - knowledge="", - routine="", - ) diff --git a/isek/adapter/simple_adapter.py b/isek/adapter/simple_adapter.py deleted file mode 100644 index 6dff67b..0000000 --- a/isek/adapter/simple_adapter.py +++ /dev/null @@ -1,35 +0,0 @@ -from isek.adapter.base import Adapter, AdapterCard - - -class SimpleAdapter(Adapter): - """A simple team implementation for testing and basic use cases.""" - - def __init__( - self, - name: str = "SimpleAdapter", - description: str = "A simple adapter for testing", - ): - self._name = name - self._description = description - - @property - def name(self) -> str: - return self._name - - @property - def description(self) -> str: - return self._description - - def run(self, prompt: str, **kwargs) -> str: - """Simple response for testing.""" - return f"{self.name} received: {prompt}" - - def get_adapter_card(self) -> AdapterCard: - """Get team card for A2A protocol.""" - return AdapterCard( - name=self.name, - bio=self.description, - lore="Created for testing purposes", - knowledge="Basic testing knowledge", - routine="Respond to messages", - ) diff --git a/isek/agent/__init__.py b/isek/agent/__init__.py deleted file mode 100644 index 14e49cf..0000000 --- a/isek/agent/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .base import BaseAgent, AgentCard -from .isek_agent import IsekAgent - -__all__ = ["BaseAgent", "AgentCard", "IsekAgent"] diff --git a/isek/agent/base.py b/isek/agent/base.py deleted file mode 100644 index 473346f..0000000 --- a/isek/agent/base.py +++ /dev/null @@ -1,302 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Callable, List, Optional, Union -from uuid import uuid4 - -from isek.memory.memory import Memory, UserMemory -from isek.models.base import Model, SimpleMessage -from isek.tools.toolkit import Toolkit -from isek.utils.log import log, LoggerManager - - -@dataclass -class AgentCard: - """Metadata about an agent for discovery and identification purposes.""" - - name: str - description: str - capabilities: List[str] - tools: List[str] - model_type: str - - -class BaseAgent(ABC): - """ - Abstract base class for all agent implementations in ISEK. - - This class defines the interface that all agent implementations must follow. - Agents are intelligent entities that can process messages, use tools, maintain memory, - and interact with language models to provide responses. - """ - - def __init__( - self, - name: Optional[str] = None, - agent_id: Optional[str] = None, - model: Optional[Model] = None, - memory: Optional[Memory] = None, - tools: Optional[List[Toolkit]] = None, - description: Optional[str] = None, - success_criteria: Optional[str] = None, - instructions: Optional[Union[str, List[str], Callable]] = None, - debug_mode: bool = False, - ): - """ - Initialize the base agent. - - Args: - name: Agent name - agent_id: Agent UUID (autogenerated if not set) - model: Model for this Agent - memory: Agent memory - tools: Tools provided to the Model - description: A description of the Agent - success_criteria: Success criteria for the task - instructions: List of instructions for the agent - debug_mode: Enable debug logs - """ - self.name = name - self.agent_id = agent_id or str(uuid4()) - self.model = model - self.memory = memory - self.tools = tools or [] - self.description = description - self.success_criteria = success_criteria - self.instructions = instructions - self.debug_mode = debug_mode - - # Set debug mode - if self.debug_mode: - LoggerManager.set_level("DEBUG") - log.debug( - f"Agent initialized: {self.name or 'Unnamed'} (ID: {self.agent_id})" - ) - - @abstractmethod - def run( - self, message: str, user_id: str = "default", session_id: Optional[str] = None - ) -> str: - """ - Run the agent with a message and return the response. - - Args: - message: The input message for the agent to process - user_id: Identifier for the user making the request - session_id: Optional session identifier for conversation tracking - - Returns: - str: The agent's response - - Raises: - NotImplementedError: If the concrete class doesn't implement this method - """ - pass - - @abstractmethod - def get_agent_card(self) -> AgentCard: - """ - Get metadata about the agent for discovery and identification purposes. - - Returns: - AgentCard: A card containing agent metadata - - Raises: - NotImplementedError: If the concrete class doesn't implement this method - """ - pass - - def _store_conversation( - self, user_id: str, session_id: str, user_message: str, agent_response: str - ) -> None: - """ - Store the conversation in memory. - - Args: - user_id: The user identifier - session_id: The session identifier - user_message: The user's message - agent_response: The agent's response - """ - if not self.memory: - return - - # Store user memory - user_memory = UserMemory( - memory=f"User: {user_message}\nAgent: {agent_response}", - topics=["conversation"], - ) - self.memory.add_user_memory(user_memory, user_id) - - # Store run - run_data = { - "user_message": user_message, - "agent_response": agent_response, - "timestamp": str(uuid4()), # Simple timestamp for now - } - self.memory.add_run(session_id, run_data) - - if self.debug_mode: - log.debug( - f"Stored conversation in memory for user {user_id}, session {session_id}" - ) - - def _build_system_message(self) -> str: - """ - Build the system message from agent configuration. - - Returns: - str: The system message for the agent - """ - parts = [] - - if self.description: - parts.append(f"Description: {self.description}") - - if self.success_criteria: - parts.append(f"Success Criteria: {self.success_criteria}") - - if self.instructions: - if isinstance(self.instructions, str): - parts.append(f"Instructions: {self.instructions}") - elif isinstance(self.instructions, list): - parts.append("Instructions:") - for instruction in self.instructions: - parts.append(f"- {instruction}") - elif callable(self.instructions): - parts.append(f"Instructions: {self.instructions()}") - - return "\n".join(parts) if parts else "You are a helpful AI assistant." - - def _prepare_tools_parameter(self) -> Optional[List[dict]]: - """ - Prepare tools parameter for model calls. - - Returns: - Optional[List[dict]]: Tools parameter for the model, or None if no tools - """ - if not self.tools: - return None - - tools_param = [] - for toolkit in self.tools: - for func in toolkit.functions.values(): - tools_param.append({"type": "function", "function": func.to_dict()}) - - return tools_param - - def _prepare_messages( - self, message: str, user_id: str, system_message: Optional[str] = None - ) -> List[SimpleMessage]: - """ - Prepare messages for model calls. - - Args: - message: The user message - user_id: The user identifier - system_message: Optional custom system message - - Returns: - List[SimpleMessage]: List of messages for the model - """ - messages = [] - - # Add system message - if system_message: - messages.append(SimpleMessage(role="system", content=system_message)) - else: - built_system_message = self._build_system_message() - if ( - built_system_message - and built_system_message != "You are a helpful AI assistant." - ): - messages.append( - SimpleMessage(role="system", content=built_system_message) - ) - - # Add memory context - memory_context = self._get_memory_context(user_id) - if memory_context: - messages.append( - SimpleMessage( - role="system", content=f"Previous context:\n{memory_context}" - ) - ) - - # Add user message - messages.append(SimpleMessage(role="user", content=message)) - - return messages - - def _get_memory_context(self, user_id: str) -> Optional[str]: - """ - Get relevant memory context for the user. - - Args: - user_id: The user identifier - - Returns: - Optional[str]: Memory context as a string, or None if no memory available - """ - if not self.memory: - return None - - memories = self.memory.get_user_memories(user_id) - if not memories: - return None - - # For now, include all memories (in a real implementation, you might want to filter by relevance) - memory_texts = [] - for memory in memories: - memory_texts.append(f"- {memory.memory}") - - return "Previous interactions:\n" + "\n".join(memory_texts) - - def get_available_tools(self) -> List[str]: - """ - Get list of available tool names. - - Returns: - List[str]: List of tool names - """ - if not self.tools: - return [] - - tool_names = [] - for toolkit in self.tools: - tool_names.extend(toolkit.functions.keys()) - return tool_names - - def has_memory(self) -> bool: - """ - Check if the agent has memory capabilities. - - Returns: - bool: True if the agent has memory, False otherwise - """ - return self.memory is not None - - def has_tools(self) -> bool: - """ - Check if the agent has tools. - - Returns: - bool: True if the agent has tools, False otherwise - """ - return self.tools is not None and len(self.tools) > 0 - - def has_model(self) -> bool: - """ - Check if the agent has a model. - - Returns: - bool: True if the agent has a model, False otherwise - """ - return self.model is not None - - def __repr__(self) -> str: - return f"BaseAgent(name='{self.name}', id='{self.agent_id}')" - - def __str__(self) -> str: - return f"Agent '{self.name or 'Unnamed'}' (ID: {self.agent_id})" diff --git a/isek/agent/isek_agent.py b/isek/agent/isek_agent.py deleted file mode 100644 index 1714233..0000000 --- a/isek/agent/isek_agent.py +++ /dev/null @@ -1,133 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Callable, List, Optional, Union, Any, Dict, Sequence -from uuid import uuid4 - -from isek.agent.base import BaseAgent, AgentCard -from isek.memory.memory import Memory -from isek.models.base import Model -from isek.tools.toolkit import Toolkit -from isek.utils.log import log -from isek.utils.print_utils import print_response - - -@dataclass -class IsekAgent(BaseAgent): - """Ultra-simplified Agent class with minimal features.""" - - # Agent name - name: Optional[str] = None - # Agent UUID (autogenerated if not set) - agent_id: Optional[str] = None - # Model for this Agent - model: Optional[Model] = None - # Agent memory - memory: Optional[Memory] = None - # Tools provided to the Model - tools: Optional[List[Toolkit]] = None - # A description of the Agent - description: Optional[str] = None - # Success criteria for the task - success_criteria: Optional[str] = None - # List of instructions for the agent - instructions: Optional[Union[str, List[str], Callable]] = None - # Enable debug logs - debug_mode: bool = False - - def __post_init__(self): - """Initialize the agent after creation.""" - # Call parent __init__ if not already called - if not hasattr(self, "agent_id"): - super().__init__( - name=self.name, - agent_id=self.agent_id, - model=self.model, - memory=self.memory, - tools=self.tools, - description=self.description, - success_criteria=self.success_criteria, - instructions=self.instructions, - debug_mode=self.debug_mode, - ) - - def run( - self, - message: str, - user_id: str = "default", - session_id: Optional[str] = None, - messages: Optional[List[Union[Dict, Any]]] = None, - audio: Optional[Sequence[Any]] = None, - images: Optional[Sequence[Any]] = None, - videos: Optional[Sequence[Any]] = None, - files: Optional[Sequence[Any]] = None, - stream: Optional[bool] = None, - stream_intermediate_steps: bool = False, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> str: - """Run the agent with a message and return the response.""" - if self.model is None: - raise ValueError("Model is required to run the agent") - - # Generate session ID if not provided - if session_id is None: - session_id = str(uuid4()) - - # Prepare messages using the base class method - messages = self._prepare_messages(message, user_id) - - # Prepare tools parameter using the base class method - tools_param = self._prepare_tools_parameter() - - # Call the model - it will handle tool calling internally - response = self.model.response( - messages=messages, - tools=tools_param, - toolkits=self.tools or [], # Pass actual toolkits for execution - ) - - response_content = response.content or "No response generated" - - # Store in memory if available - if self.memory: - self._store_conversation(user_id, session_id, message, response_content) - - if self.debug_mode: - log.debug(f"Session ID: {session_id}") - log.debug(f"User ID: {user_id}") - log.debug(f"User message: {message}") - log.debug(f"Model response: {response_content}") - if tools_param: - log.debug(f"Tools: {tools_param}") - - return response_content - - def print_response(self, *args, **kwargs): - """ - Proxy to the shared print_response utility, passing self.run as run_func. - """ - return print_response(self.run, *args, **kwargs) - - def get_agent_card(self) -> AgentCard: - """Get metadata about the agent for discovery and identification purposes.""" - capabilities = [] - if self.has_memory(): - capabilities.append("memory") - if self.has_tools(): - capabilities.append("tools") - if self.has_model(): - capabilities.append("llm") - - model_type = type(self.model).__name__ if self.model else "None" - - return AgentCard( - name=self.name or "Unnamed Agent", - description=self.description or "No description", - capabilities=capabilities, - tools=self.get_available_tools(), - model_type=model_type, - ) - - def __repr__(self) -> str: - return f"IsekAgent(name='{self.name}', id='{self.agent_id}')" diff --git a/isek/isek_center.py b/isek/isek_center.py deleted file mode 100644 index e074795..0000000 --- a/isek/isek_center.py +++ /dev/null @@ -1,175 +0,0 @@ -from isek.utils.log import team_log -import uvicorn -import threading -import time -from typing import Dict, Any, Optional, Tuple - -from flask import Flask, request, jsonify, Blueprint - -# --- Global State & Configuration --- -isek_center_blueprint = Blueprint( - "isek_center_blueprint", __name__, url_prefix="/isek_center" -) - -# In-memory storage for registered nodes. -# Structure: { "node_id": {"node_id": str, "host": str, "port": int, "metadata": dict, "expires_at": float} } -nodes: Dict[str, Dict[str, Any]] = {} -NODE_LOCK = threading.Lock() - -LEASE_DURATION: int = 30 - -# --- Type Alias --- -FlaskResponse = Tuple[Dict[str, Any], int] - - -# --- Response Helper Class --- -class CommonResponse: - """A helper class to standardize JSON API responses.""" - - def __init__(self, data: Optional[Any], code: int, message: str): - self.data: Optional[Any] = data - self.code: int = code - self.message: str = message - - @classmethod - def success( - cls, data: Optional[Any] = None, code: int = 200, message: str = "success" - ) -> FlaskResponse: - return jsonify(cls(data, code, message).to_dict()), code - - @classmethod - def fail( - cls, message: str, code: int = 400, data: Optional[Any] = None - ) -> FlaskResponse: - return jsonify(cls(data, code, message).to_dict()), code - - def to_dict(self) -> Dict[str, Any]: - return {"code": self.code, "message": self.message, "data": self.data} - - -# --- Flask Route Definitions --- -@isek_center_blueprint.route("/register", methods=["POST"]) -def register_node_route() -> FlaskResponse: - data = request.json - if not data: - return CommonResponse.fail(message="Request body must be JSON.", code=400) - - node_id = data.get("node_id") - host = data.get("host") - port = data.get("port") - metadata = data.get("metadata") - - if not all([node_id, host, port]): - return CommonResponse.fail( - message="'node_id', 'host', and 'port' are required fields.", code=400 - ) - - if not isinstance(port, int): - return CommonResponse.fail(message="'port' must be an integer.", code=400) - - with NODE_LOCK: - nodes[node_id] = { - "node_id": node_id, - "host": host, - "port": port, - "metadata": metadata or {}, - "expires_at": time.time() + LEASE_DURATION, - } - team_log.info(f"Node registered/updated: {node_id}") - return CommonResponse.success(message=f"Node '{node_id}' registered successfully.") - - -@isek_center_blueprint.route("/deregister", methods=["POST"]) -def deregister_node_route() -> FlaskResponse: - data = request.json - if not data: - return CommonResponse.fail(message="Request body must be JSON.", code=400) - - node_id = data.get("node_id") - - if not node_id: - return CommonResponse.fail(message="'node_id' is required.", code=400) - - with NODE_LOCK: - if node_id not in nodes: - return CommonResponse.fail( - message=f"Node '{node_id}' not found for deregistration.", code=404 - ) - removed_node = nodes.pop(node_id) - team_log.debug(f"Node deregistered: {node_id}, Details: {removed_node}") - return CommonResponse.success( - message=f"Node '{node_id}' deregistered successfully." - ) - - -@isek_center_blueprint.route("/available_nodes", methods=["GET"]) -def get_available_nodes_route() -> FlaskResponse: - current_time = time.time() - with NODE_LOCK: - active_nodes = { - k: v for k, v in nodes.items() if v.get("expires_at", 0) > current_time - } - - response_payload = {"available_nodes": active_nodes} - team_log.debug(f"Returning {len(active_nodes)} available nodes.") - return CommonResponse.success(data=response_payload) - - -@isek_center_blueprint.route("/renew", methods=["POST"]) -def renew_lease_route() -> FlaskResponse: - data = request.json - if not data: - return CommonResponse.fail(message="Request body must be JSON.", code=400) - - node_id = data.get("node_id") - - if not node_id: - return CommonResponse.fail(message="'node_id' is required.", code=400) - - with NODE_LOCK: - if node_id in nodes: - nodes[node_id]["expires_at"] = time.time() + LEASE_DURATION - team_log.debug(f"Lease renewed for node: {node_id}") - return CommonResponse.success( - message=f"Lease for node '{node_id}' renewed successfully." - ) - else: - return CommonResponse.fail( - message=f"Node '{node_id}' not found for lease renewal.", code=404 - ) - - -# --- Background Task --- -def cleanup_expired_nodes(): - """Periodically removes expired nodes from the registry.""" - while True: - time.sleep(LEASE_DURATION / 2) - current_time = time.time() - with NODE_LOCK: - expired_node_ids = [ - node_id - for node_id, data in nodes.items() - if data.get("expires_at", 0) < current_time - ] - for node_id in expired_node_ids: - team_log.info(f"Node lease expired, removing: {node_id}") - del nodes[node_id] - - -# --- Main Application Factory and Entry Point --- -def main(): - """Main function to run the Isek Center.""" - app = Flask(__name__) - app.register_blueprint(isek_center_blueprint) - - # Start the background task for cleaning up expired nodes - cleanup_thread = threading.Thread(target=cleanup_expired_nodes, daemon=True) - cleanup_thread.start() - - team_log.info("Starting Isek Center...") - # Use uvicorn to run the Flask app for better performance - uvicorn.run(app, host="0.0.0.0", port=8088, log_level="info") - - -if __name__ == "__main__": - main() diff --git a/isek/memory/memory.py b/isek/memory/memory.py deleted file mode 100644 index c8ad5e4..0000000 --- a/isek/memory/memory.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from datetime import datetime -from typing import Any, Dict, List, Optional -from uuid import uuid4 - -from pydantic import BaseModel - - -class UserMemory(BaseModel): - """Simple user memory model.""" - - memory_id: Optional[str] = None - memory: str - topics: Optional[List[str]] = None - last_updated: Optional[datetime] = None - - def __post_init__(self): - if self.memory_id is None: - self.memory_id = str(uuid4()) - if self.last_updated is None: - self.last_updated = datetime.now() - - def to_dict(self) -> Dict[str, Any]: - return self.model_dump(exclude_none=True) - - -class SessionSummary(BaseModel): - """Simple session summary model.""" - - summary: str - topics: Optional[List[str]] = None - last_updated: Optional[datetime] = None - - def __post_init__(self): - if self.last_updated is None: - self.last_updated = datetime.now() - - def to_dict(self) -> Dict[str, Any]: - return self.model_dump(exclude_none=True) - - -@dataclass -class Memory: - """Ultra-simplified Memory class with minimal features.""" - - # Model used for memories and summaries (optional) - model: Optional[Any] = None - - # Simple in-memory storage - memories: Dict[str, Dict[str, UserMemory]] = field(default_factory=dict) - summaries: Dict[str, Dict[str, SessionSummary]] = field(default_factory=dict) - runs: Dict[str, List[Any]] = field(default_factory=dict) - - # Debug mode - debug_mode: bool = False - version: int = 2 - - def __post_init__(self): - """Initialize memory after creation.""" - if self.debug_mode: - print(f"Memory initialized (version: {self.version})") - - def add_user_memory(self, memory: UserMemory, user_id: str = "default") -> str: - """Add a user memory.""" - if memory.memory_id is None: - memory.memory_id = str(uuid4()) - - if user_id not in self.memories: - self.memories[user_id] = {} - - self.memories[user_id][memory.memory_id] = memory - - if self.debug_mode: - print(f"Added memory for user {user_id}: {memory.memory_id}") - - return memory.memory_id - - def get_user_memories(self, user_id: str = "default") -> List[UserMemory]: - """Get all memories for a user.""" - if user_id not in self.memories: - return [] - return list(self.memories[user_id].values()) - - def get_user_memory( - self, memory_id: str, user_id: str = "default" - ) -> Optional[UserMemory]: - """Get a specific memory by ID.""" - if user_id not in self.memories: - return None - return self.memories[user_id].get(memory_id) - - def delete_user_memory(self, memory_id: str, user_id: str = "default") -> bool: - """Delete a user memory.""" - if user_id in self.memories and memory_id in self.memories[user_id]: - del self.memories[user_id][memory_id] - if self.debug_mode: - print(f"Deleted memory {memory_id} for user {user_id}") - return True - return False - - def add_session_summary( - self, session_id: str, summary: SessionSummary, user_id: str = "default" - ) -> str: - """Add a session summary.""" - if user_id not in self.summaries: - self.summaries[user_id] = {} - - self.summaries[user_id][session_id] = summary - - if self.debug_mode: - print(f"Added session summary for user {user_id}, session {session_id}") - - return session_id - - def get_session_summary( - self, session_id: str, user_id: str = "default" - ) -> Optional[SessionSummary]: - """Get a session summary.""" - if user_id not in self.summaries: - return None - return self.summaries[user_id].get(session_id) - - def add_run(self, session_id: str, run: Any) -> None: - """Add a run to memory.""" - if session_id not in self.runs: - self.runs[session_id] = [] - - self.runs[session_id].append(run) - - if self.debug_mode: - print(f"Added run to session {session_id}") - - def get_runs(self, session_id: str) -> List[Any]: - """Get all runs for a session.""" - return self.runs.get(session_id, []) - - def clear(self) -> None: - """Clear all memory.""" - self.memories.clear() - self.summaries.clear() - self.runs.clear() - - if self.debug_mode: - print("Memory cleared") - - def to_dict(self) -> Dict[str, Any]: - """Convert memory to dictionary.""" - return { - "memories": { - user_id: { - memory_id: memory.to_dict() - for memory_id, memory in user_memories.items() - } - for user_id, user_memories in self.memories.items() - }, - "summaries": { - user_id: { - session_id: summary.to_dict() - for session_id, summary in session_summaries.items() - } - for user_id, session_summaries in self.summaries.items() - }, - "runs": { - session_id: [str(run) for run in runs] - for session_id, runs in self.runs.items() - }, - } - - def __repr__(self) -> str: - return f"Memory(users={len(self.memories)}, sessions={len(self.summaries)}, runs={len(self.runs)})" diff --git a/isek/models/__init__.py b/isek/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/isek/models/base.py b/isek/models/base.py deleted file mode 100644 index 6606249..0000000 --- a/isek/models/base.py +++ /dev/null @@ -1,291 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Any, Dict, List, Optional - - -@dataclass -class SimpleMessage: - """Ultra-simplified message model.""" - - role: str - content: Optional[str] = None - name: Optional[str] = None - tool_call_id: Optional[str] = None - tool_calls: Optional[list] = None - - def to_dict(self) -> Dict[str, Any]: - d = { - "role": self.role, - "content": self.content, - } - if self.name: - d["name"] = self.name - if self.role == "tool" and self.tool_call_id: - d["tool_call_id"] = self.tool_call_id - if self.tool_calls is not None: - d["tool_calls"] = self.tool_calls - return d - - -@dataclass -class SimpleModelResponse: - """Ultra-simplified model response.""" - - content: Optional[str] = None - role: Optional[str] = None - tool_calls: Optional[List[Dict[str, Any]]] = None - extra: Optional[Dict[str, Any]] = None - - def to_dict(self) -> Dict[str, Any]: - return { - "content": self.content, - "role": self.role, - "tool_calls": self.tool_calls, - "extra": self.extra, - } - - -class Model(ABC): - """Ultra-simplified abstract base model class.""" - - def __init__( - self, - id: str, - name: Optional[str] = None, - provider: Optional[str] = None, - ): - """Initialize the model. - - Args: - id: The model ID - name: The model name - provider: The model provider - """ - self.id = id - self.name = name or id - self.provider = provider or "unknown" - - # Basic configuration - self.supports_native_structured_outputs: bool = False - self.supports_json_schema_outputs: bool = False - self.tool_message_role: str = "tool" - self.assistant_message_role: str = "assistant" - - def get_provider(self) -> str: - """Get the provider name.""" - return self.provider - - def to_dict(self) -> Dict[str, Any]: - """Convert model to dictionary.""" - return {"id": self.id, "name": self.name, "provider": self.provider} - - @abstractmethod - def invoke(self, messages: List[SimpleMessage], **kwargs) -> Any: - """Invoke the model with messages. - - Args: - messages: List of messages to send to the model - **kwargs: Additional arguments - - Returns: - Raw response from the model - """ - pass - - @abstractmethod - async def ainvoke(self, messages: List[SimpleMessage], **kwargs) -> Any: - """Async invoke the model with messages. - - Args: - messages: List of messages to send to the model - **kwargs: Additional arguments - - Returns: - Raw response from the model - """ - pass - - @abstractmethod - def parse_provider_response(self, response: Any, **kwargs) -> SimpleModelResponse: - """Parse the raw response from the model provider. - - Args: - response: Raw response from the model provider - **kwargs: Additional arguments - - Returns: - Parsed model response - """ - pass - - def response(self, messages: List[SimpleMessage], **kwargs) -> SimpleModelResponse: - """Generate a response from the model. - - Args: - messages: List of messages to send to the model - **kwargs: Additional arguments including: - - tools: List of tool schemas for the model - - toolkits: List of actual toolkits for execution - Returns: - Parsed model response - """ - # Check if tools are provided - tools = kwargs.get("tools") - toolkits = kwargs.get("toolkits", []) - - if not tools: - # No tools, simple single call - raw_response = self.invoke(messages, **kwargs) - return self.parse_provider_response(raw_response, **kwargs) - - # Tools provided, handle tool calling loop internally - messages_for_model = messages.copy() - - for _ in range(10): # Prevent infinite loops - # Call the model - raw_response = self.invoke(messages_for_model, **kwargs) - model_response = self.parse_provider_response(raw_response, **kwargs) - - # If the model returns a final text response (no tool calls), return it - if model_response.content and not model_response.tool_calls: - return model_response - - # If the model requests tool calls, execute them and continue - if model_response.tool_calls: - # Add the assistant message (with tool_calls) to the conversation history - assistant_msg = SimpleMessage( - role="assistant", - content="" if model_response.tool_calls else model_response.content, - tool_calls=model_response.tool_calls, - ) - messages_for_model.append(assistant_msg) - - # Execute each tool call and add results - tool_messages = [] - for tool_call in model_response.tool_calls: - tool_name = tool_call.get("function", {}).get("name") - tool_args = tool_call.get("function", {}).get("arguments") - tool_call_id = tool_call.get("id") - - # Parse tool arguments - if isinstance(tool_args, str): - import json - - try: - tool_args = json.loads(tool_args) - except Exception: - tool_args = {} - if not isinstance(tool_args, dict): - tool_args = {} - - # Execute the tool using the provided toolkits - tool_result = self._execute_tool(tool_name, tool_args, toolkits) - - # Add tool result to conversation - tool_msg = SimpleMessage( - role="tool", content=str(tool_result), tool_call_id=tool_call_id - ) - tool_messages.append(tool_msg) - # Find the last assistant message (with tool_calls) - last_assistant_idx = None - for i in range(len(messages_for_model) - 1, -1, -1): - msg = messages_for_model[i] - if getattr(msg, "role", None) == "assistant": - last_assistant_idx = i - break - if last_assistant_idx is not None: - messages_for_model = ( - messages_for_model[: last_assistant_idx + 1] + tool_messages - ) - else: - messages_for_model = messages_for_model + tool_messages - else: - # If neither content nor tool_calls, return what we have - return model_response - - # If we reach here, we hit the loop limit - return model_response - - def _execute_tool(self, tool_name: str, tool_args: dict, toolkits: List) -> str: - """Execute a tool by name with arguments using the provided toolkits. - - Args: - tool_name: Name of the tool to execute - tool_args: Arguments for the tool - toolkits: List of toolkits to search for the tool - - Returns: - Result of tool execution as string - """ - # Search for the tool in the provided toolkits - for toolkit in toolkits: - if hasattr(toolkit, "functions") and tool_name in toolkit.functions: - try: - result = toolkit.execute_function(tool_name, **(tool_args or {})) - return str(result) - except Exception as e: - return f"Error executing tool '{tool_name}': {e}" - - return f"Tool '{tool_name}' not found in any toolkit" - - async def aresponse( - self, messages: List[SimpleMessage], **kwargs - ) -> SimpleModelResponse: - """Generate an async response from the model. - - Args: - messages: List of messages to send to the model - **kwargs: Additional arguments - - Returns: - Parsed model response - """ - # Get raw response from model (pass SimpleMessage objects directly) - raw_response = await self.ainvoke(messages, **kwargs) - - # Parse the response - return self.parse_provider_response(raw_response, **kwargs) - - def _format_messages(self, messages: List[SimpleMessage]) -> List[Dict[str, Any]]: - """Format messages for the model provider. - - Args: - messages: List of SimpleMessage objects - - Returns: - List of formatted message dictionaries - """ - return [msg.to_dict() for msg in messages] - - def __repr__(self) -> str: - return f"<{self.__class__.__name__} id='{self.id}' provider='{self.provider}'>" - - def __str__(self) -> str: - return self.__repr__() - - -# Example concrete implementation -class SimpleModel(Model): - """Simple model implementation for testing.""" - - def invoke(self, messages: List[SimpleMessage], **kwargs) -> Any: - """Simple mock implementation.""" - # Just return the last user message as a response - for msg in reversed(messages): - if msg.role == "user" and msg.content: - return {"content": f"Echo: {msg.content}"} - return {"content": "No user message found"} - - async def ainvoke(self, messages: List[SimpleMessage], **kwargs) -> Any: - """Simple async mock implementation.""" - return self.invoke(messages, **kwargs) - - def parse_provider_response(self, response: Any, **kwargs) -> SimpleModelResponse: - """Parse the mock response.""" - if isinstance(response, dict): - return SimpleModelResponse( - content=response.get("content"), role="assistant" - ) - return SimpleModelResponse(content=str(response), role="assistant") diff --git a/isek/models/litellm/__init__.py b/isek/models/litellm/__init__.py deleted file mode 100644 index d611e33..0000000 --- a/isek/models/litellm/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from isek.models.litellm.chat import LiteLLMModel - -__all__ = [ - "LiteLLMModel", -] diff --git a/isek/models/litellm/chat.py b/isek/models/litellm/chat.py deleted file mode 100644 index 5dac503..0000000 --- a/isek/models/litellm/chat.py +++ /dev/null @@ -1,190 +0,0 @@ -"""LiteLLM model implementation.""" - -import os -from typing import Any, List, Optional -from litellm import completion - -from isek.models.base import Model, SimpleMessage, SimpleModelResponse -from isek.models.provider import PROVIDER_MAP, DEFAULT_PROVIDER -from isek.utils.log import log - - -class LiteLLMModel(Model): - """Ultra-simplified LiteLLM model implementation.""" - - def __init__( - self, - provider: Optional[str] = None, - model_id: Optional[str] = None, - api_key: Optional[str] = None, - base_url: Optional[str] = None, - ): - """Initialize the LiteLLM model. - - Args: - provider: The provider name (e.g., "openai", "anthropic", "gemini") - model_id: The model ID (e.g., "gpt-3.5-turbo", "claude-3-sonnet") - api_key: Optional API key for the provider (if required) - base_url: Custom base URL for the API - """ - # Get provider with fallback - _provider = provider or DEFAULT_PROVIDER - if _provider not in PROVIDER_MAP: - raise ValueError( - f"Unsupported provider: {_provider}. " - f"Supported providers are: {', '.join(PROVIDER_MAP.keys())}." - ) - - # Get provider configuration - provider_config = PROVIDER_MAP[_provider] - model_env_key = provider_config.get("model_env_key") - default_model = provider_config.get("default_model") - api_env_key = provider_config.get("api_env_key") # May be None - base_url_env_key = provider_config.get("base_url_env_key") # May be None - - # Validate required configuration (model_env_key and default_model are mandatory) - if not model_env_key or not default_model: - raise ValueError( - f"Invalid provider configuration for {_provider}. " - f"Missing model_env_key or default_model." - ) - - # Get model ID with fallback - _model_id = model_id or os.environ.get(model_env_key, default_model) - - # Initialize base class - super().__init__(id=_model_id, name=_model_id, provider=_provider) - - # Set capabilities - self.supports_native_structured_outputs = True - self.supports_json_schema_outputs = True - - # Store configuration - self.api_key = None - if api_env_key or api_key: # Only set api_key if provider supports it - self.api_key = api_key or os.environ.get(api_env_key) - if not self.api_key and provider_config.get("requires_api_key", True): - raise ValueError( - f"API key is required for provider {_provider}, " - f"but none was provided or found in {api_env_key}." - ) - - self.base_url = base_url or ( - os.environ.get(base_url_env_key) if base_url_env_key else None - ) - - log.info(f"LiteLLMModel initialized: {self.id} (provider: {_provider})") - - def invoke(self, messages: List[SimpleMessage], **kwargs: Any) -> Any: - """Invoke the LiteLLM model. - - Args: - messages: List of messages to send - **kwargs: Additional arguments for the API call - - Returns: - Raw ModelResponse - """ - # Convert SimpleMessage to LiteLLM format - formatted_messages = [] - for msg in messages: - formatted_msg = {"role": msg.role, "content": msg.content} - if msg.name: - formatted_msg["name"] = msg.name - if msg.role == "tool" and msg.tool_call_id: - formatted_msg["tool_call_id"] = msg.tool_call_id - if msg.tool_calls is not None: - formatted_msg["tool_calls"] = msg.tool_calls - formatted_messages.append(formatted_msg) - - # Prepare request parameters - params = { - "model": self.id, - "messages": formatted_messages, - } - # Only add 'tools' if present in kwargs and not None - if "tools" in kwargs and kwargs["tools"]: - params["tools"] = kwargs.pop("tools") - # Filter out 'toolkits' parameter as LiteLLM doesn't expect it - kwargs.pop("toolkits", None) - # Add any other kwargs - params.update(kwargs) - - log.debug(f"LiteLLM request: {self.id}") - - try: - response = completion(**params) - log.debug("LiteLLM response received") - return response - except Exception as e: - log.error(f"LiteLLM API error: {e}") - raise - - async def ainvoke(self, messages: List[SimpleMessage], **kwargs: Any) -> Any: - """Async invoke the LiteLLM model. - - Args: - messages: List of messages to send - **kwargs: Additional arguments for the API call - - Returns: - Raw ModelResponse - """ - # For now, just call the sync version - # TODO: Implement proper async when needed - return self.invoke(messages, **kwargs) - - def parse_provider_response( - self, response: Any, **kwargs: Any - ) -> SimpleModelResponse: - """Parse the LiteLLM response. - - Args: - response: Raw ModelResponse - **kwargs: Additional arguments - - Returns: - Parsed SimpleModelResponse - """ - if not response.choices: - return SimpleModelResponse(content="No response generated") - - choice = response.choices[0] - - # Safely access message - if hasattr(choice, "message"): - message = choice.message - else: - # For streaming responses, the choice might be the message itself - message = choice - - # Extract tool calls if present - tool_calls = None - if hasattr(message, "tool_calls") and message.tool_calls: - tool_calls = [] - for tool_call in message.tool_calls: - tool_calls.append( - { - "id": tool_call.id, - "type": "function", - "function": { - "name": tool_call.function.name, - "arguments": tool_call.function.arguments, - }, - } - ) - - # Extract extra information - extra = { - "finish_reason": getattr(choice, "finish_reason", None), - "usage": getattr(response, "usage", None), - "model": getattr(response, "model", None), - "id": getattr(response, "id", None), - } - - return SimpleModelResponse( - content=getattr(message, "content", None), - role=getattr(message, "role", "assistant"), - tool_calls=tool_calls, - extra=extra, - ) diff --git a/isek/models/openai/__init__.py b/isek/models/openai/__init__.py deleted file mode 100644 index cfa4d0c..0000000 --- a/isek/models/openai/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from isek.models.openai.openai import OpenAIModel - -__all__ = [ - "OpenAIModel", -] diff --git a/isek/models/openai/openai.py b/isek/models/openai/openai.py deleted file mode 100644 index be85547..0000000 --- a/isek/models/openai/openai.py +++ /dev/null @@ -1,154 +0,0 @@ -"""OpenAI model implementation.""" - -import os -from typing import Any, List, Optional -from openai import OpenAI -from openai.types.chat import ChatCompletion - -from isek.models.base import Model, SimpleMessage, SimpleModelResponse -from isek.utils.log import log - - -class OpenAIModel(Model): - """Ultra-simplified OpenAI model implementation.""" - - def __init__( - self, - model_id: Optional[str] = None, - api_key: Optional[str] = None, - base_url: Optional[str] = None, - ): - """Initialize the OpenAI model. - - Args: - model_id: The OpenAI model ID (e.g., "gpt-3.5-turbo", "gpt-4") - api_key: OpenAI API key - base_url: Custom base URL for the API - """ - # Get model ID with fallback - _model_id = model_id or os.environ.get("OPENAI_MODEL_NAME", "gpt-3.5-turbo") - - # Initialize base class - super().__init__(id=_model_id, name=_model_id, provider="openai") - - # Set capabilities - self.supports_native_structured_outputs = True - self.supports_json_schema_outputs = True - - # Initialize OpenAI client - _api_key = api_key or os.environ.get("OPENAI_API_KEY") - _base_url = base_url or os.environ.get("OPENAI_BASE_URL") - - self.client = OpenAI(api_key=_api_key, base_url=_base_url) - - log.debug(f"OpenAIModel initialized: {self.id}") - - def invoke(self, messages: List[SimpleMessage], **kwargs: Any) -> ChatCompletion: - """Invoke the OpenAI model. - - Args: - messages: List of messages to send - **kwargs: Additional arguments for the API call - - Returns: - Raw ChatCompletion response - """ - # Convert SimpleMessage to OpenAI format - formatted_messages = [] - for msg in messages: - formatted_msg = {"role": msg.role, "content": msg.content} - if msg.name: - formatted_msg["name"] = msg.name - if msg.role == "tool" and msg.tool_call_id: - formatted_msg["tool_call_id"] = msg.tool_call_id - if msg.tool_calls is not None: - formatted_msg["tool_calls"] = msg.tool_calls - formatted_messages.append(formatted_msg) - - # Prepare request parameters - params = { - "model": self.id, - "messages": formatted_messages, - } - # Only add 'tools' if present in kwargs and not None - if "tools" in kwargs and kwargs["tools"]: - params["tools"] = kwargs.pop("tools") - # Filter out 'toolkits' parameter as OpenAI doesn't expect it - kwargs.pop("toolkits", None) - # Add any other kwargs - params.update(kwargs) - - log.debug(f"OpenAI request: {self.id}") - - try: - response = self.client.chat.completions.create(**params) - log.debug(f"OpenAI response: {response.id}") - return response - except Exception as e: - log.error(f"OpenAI API error: {e}") - raise - - async def ainvoke( - self, messages: List[SimpleMessage], **kwargs: Any - ) -> ChatCompletion: - """Async invoke the OpenAI model. - - Args: - messages: List of messages to send - **kwargs: Additional arguments for the API call - - Returns: - Raw ChatCompletion response - """ - # For now, just call the sync version - # TODO: Implement proper async when needed - return self.invoke(messages, **kwargs) - - def parse_provider_response( - self, response: ChatCompletion, **kwargs: Any - ) -> SimpleModelResponse: - """Parse the OpenAI response. - - Args: - response: Raw ChatCompletion response - **kwargs: Additional arguments - - Returns: - Parsed SimpleModelResponse - """ - if not response.choices: - return SimpleModelResponse(content="No response generated") - - choice = response.choices[0] - message = choice.message - - # Extract tool calls if present - tool_calls = None - if message.tool_calls: - tool_calls = [] - for tool_call in message.tool_calls: - tool_calls.append( - { - "id": tool_call.id, - "type": tool_call.type, - "function": { - "name": tool_call.function.name, - "arguments": tool_call.function.arguments, - }, - } - ) - - # Extract extra information - extra = { - "finish_reason": choice.finish_reason, - "usage": response.usage.model_dump() if response.usage else None, - "model": response.model, - "id": response.id, - } - - return SimpleModelResponse( - content=message.content, - role=message.role, - tool_calls=tool_calls, - extra=extra, - ) diff --git a/isek/models/provider.py b/isek/models/provider.py deleted file mode 100644 index 58c0412..0000000 --- a/isek/models/provider.py +++ /dev/null @@ -1,34 +0,0 @@ -PROVIDER_MAP = { - "openai": { - "model_env_key": "OPENAI_MODEL_NAME", - "api_env_key": "OPENAI_API_KEY", - "default_model": "gpt-4o-mini", - "base_url_env_key": "OPENAI_BASE_URL", - }, - "anthropic": { - "model_env_key": "ANTHROPIC_MODEL_NAME", - "api_env_key": "ANTHROPIC_API_KEY", - "default_model": "claude-2", - "base_url_env_key": "ANTHROPIC_BASE_URL", - }, - "gemini": { - "model_env_key": "GEMINI_MODEL_NAME", - "api_env_key": "GEMINI_API_KEY", - "default_model": "gemini-2.0-flash", - "base_url_env_key": "GEMINI_BASE_URL", - }, - "deepseek": { - "model_env_key": "DEEPSEEK_MODEL_NAME", - "api_env_key": "DEEPSEEK_API_KEY", - "default_model": "deepseek-2.0", - "base_url_env_key": "DEEPSEEK_BASE_URL", - }, - "ollama": { - "model_env_key": "OLLAMA_MODEL_NAME", - "default_model": "deepseek-coder:6.7b", - "base_url_env_key": "OLLAMA_BASE_URL", - }, -} - -DEFAULT_PROVIDER = "openai" -DEFAULT_MODEL = "gpt-4o-mini" # Default model for OpenAI provider diff --git a/isek/models/simpleModel.py b/isek/models/simpleModel.py deleted file mode 100644 index ffcb01a..0000000 --- a/isek/models/simpleModel.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -from typing import Any, List -from isek.models.base import SimpleMessage, SimpleModelResponse -from isek.models.base import Model - - -class SimpleModel(Model): - """Simple model implementation for testing that echoes user messages.""" - - def __init__(self, model_id: str = "simple-model"): - super().__init__(id=model_id) - - def invoke(self, messages: List[SimpleMessage], **kwargs) -> Any: - """Simple mock implementation.""" - # Just return the last user message as a response - for msg in reversed(messages): - if msg.role == "user" and msg.content: - return {"content": f"Echo: {msg.content}"} - return {"content": "No user message found"} - - async def ainvoke(self, messages: List[SimpleMessage], **kwargs) -> Any: - """Simple async mock implementation.""" - return self.invoke(messages, **kwargs) - - def parse_provider_response(self, response: Any, **kwargs) -> SimpleModelResponse: - """Parse the mock response.""" - if isinstance(response, dict): - return SimpleModelResponse( - content=response.get("content"), role="assistant" - ) - return SimpleModelResponse(content=str(response), role="assistant") diff --git a/isek/node/default_registry.py b/isek/node/default_registry.py deleted file mode 100644 index 2650efe..0000000 --- a/isek/node/default_registry.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Optional, Dict - -from isek.node.registry import Registry -from isek.utils.log import log - - -class DefaultRegistry(Registry): - def register_node( - self, - node_id: str, - host: str, - port: int, - metadata: Optional[Dict[str, str]] = None, - ): - log.debug(f"Node {node_id} default registered.") - - def get_available_nodes(self) -> dict: - return {} - - def deregister_node(self, node_id: str): - log.debug(f"Node {node_id} default deregistered.") - - def lease_refresh(self, node_id: str): - log.debug(f"Node {node_id} default lease refresh.") diff --git a/isek/node/etcd_registry.py b/isek/node/etcd_registry.py deleted file mode 100644 index db6f03d..0000000 --- a/isek/node/etcd_registry.py +++ /dev/null @@ -1,150 +0,0 @@ -import json -from typing import Optional, Dict - -import etcd3gw -import base64 -from ecdsa.keys import SigningKey, VerifyingKey -from ecdsa.curves import NIST256p - -from isek.utils.log import log -from isek.node.registry import Registry - - -class EtcdRegistry(Registry): - def __init__( - self, - host: Optional[str] = None, - port: Optional[int] = None, - parent_node_id: Optional[str] = "root", - etcd_client: Optional[etcd3gw.Etcd3Client] = None, - ttl: int = 30, - ): - if host and port and etcd_client: - log.warning( - "Both 'host/port' and 'etcd_client' provided. Using 'etcd_client'." - ) - - if etcd_client: - self.etcd_client = etcd_client - elif host and port: - self.etcd_client = etcd3gw.client(host=host, port=port) - else: - raise TypeError( - "Either 'host' and 'port' or 'etcd_client' must be provided." - ) - - self.parent_node_id = parent_node_id or "root" - - if not self.etcd_client.status(): - raise ConnectionError("Failed to connect to the etcd server.") - - self.sk = SigningKey.generate(curve=NIST256p) - - self.ttl = ttl - self.leases: Dict[str, etcd3gw.Lease] = {} - - def register_node( - self, - node_id: str, - host: str, - port: int, - metadata: Optional[Dict[str, str]] = None, - ): - vk = self.sk.verifying_key - if vk is None: - raise ValueError("Verifying key could not be generated.") - vk_bytes = vk.to_string() - vk_base64 = base64.b64encode(vk_bytes).decode("utf-8") - - node_info = { - "node_id": node_id, - "host": host, - "port": port, - "public_key": vk_base64, - "metadata": metadata or {}, - } - - node_info_json = json.dumps(node_info, sort_keys=True).encode("utf-8") - signature = base64.b64encode(self.sk.sign_deterministic(node_info_json)).decode( - "utf-8" - ) - - node_entry = {"node_info": node_info, "signature": signature} - - lease = self.etcd_client.lease(self.ttl) - if lease: - self.leases[node_id] = lease - - key = f"/{self.parent_node_id}/{node_id}" - self.etcd_client.put(key, json.dumps(node_entry), lease=lease) - - log.info(f"Node {node_id} has been registered to etcd.") - - def lease_refresh(self, node_id: str): - lease_refresh_response = None - try: - self.__verify_signature(node_id) - if node_id in self.leases: - self.leases[node_id].refresh() - # log.debug(f"Lease renewed for node: {node_id}, response: {lease_refresh_response}") - except Exception as e: - log.exception( - f"Lease renewal failed for node {node_id}, response: {lease_refresh_response}: {e}" - ) - - def get_available_nodes(self) -> Dict[str, dict]: - nodes = {} - key_prefix = f"/{self.parent_node_id}/" - for value, metadata in self.etcd_client.get_prefix(key_prefix): - if not isinstance(metadata, dict) or "key" not in metadata: - continue - - key_bytes = metadata.get("key") - if not isinstance(key_bytes, bytes): - continue - - node_id = key_bytes.decode("utf-8").split(key_prefix)[-1] - try: - if isinstance(value, bytes): - nodes[node_id] = json.loads(value.decode("utf-8"))["node_info"] - except Exception as e: - log.exception(f"Error decoding node {node_id}: {e}") - return nodes - - def deregister_node(self, node_id: str): - key = f"/{self.parent_node_id}/{node_id}" - self.__verify_signature(node_id) - self.etcd_client.delete(key) - if node_id in self.leases: - self.leases[node_id].revoke() - del self.leases[node_id] - log.info(f"Node {node_id} deregistered.") - - def __verify_signature(self, node_id): - key = f"/{self.parent_node_id}/{node_id}" - result = self.etcd_client.get(key) - - if not result or not result[0]: - raise ValueError(f"Node {node_id} not found") - - node_entry_json = result[0] - if not isinstance(node_entry_json, (bytes, str)): - raise TypeError(f"Expected bytes or str, but got {type(node_entry_json)}") - - node_entry = json.loads(node_entry_json) - node_info = node_entry["node_info"] - node_base64_signature = node_entry["signature"] - - vk_bytes = base64.b64decode(node_info["public_key"]) - vk = VerifyingKey.from_string(vk_bytes, curve=NIST256p) - - node_info_json = json.dumps(node_info, sort_keys=True).encode("utf-8") - - signature_bytes = base64.b64decode(node_base64_signature) - - try: - vk.verify(signature_bytes, node_info_json) - except Exception as e: - raise ValueError( - f"Signature verification failed for node {node_id}! Reason: {e}" - ) diff --git a/isek/node/isek_center_registry.py b/isek/node/isek_center_registry.py deleted file mode 100644 index fe000f4..0000000 --- a/isek/node/isek_center_registry.py +++ /dev/null @@ -1,286 +0,0 @@ -import json -from typing import Optional, Dict, Any # Added Any - -import requests # type: ignore # If requests doesn't have stubs or for explicit ignoring -from requests.exceptions import RequestException # For better error handling - -from isek.utils.log import log # Assuming logger is configured -from isek.node.registry import Registry # Assuming Registry is an ABC or base class - -# Type alias for node metadata and node info -NodeMetadata = Dict[str, str] -NodeInfo = Dict[ - str, Any -] # e.g., {"node_id": str, "host": str, "port": int, "metadata": NodeMetadata} - - -class IsekCenterRegistry(Registry): - """ - An implementation of the :class:`~isek.node.registry.Registry` interface - that interacts with a centralized "Isek Center" service via HTTP API calls. - - This registry delegates node registration, deregistration, lease renewal, - and discovery to an external Isek Center. All operations involve sending - HTTP requests to predefined endpoints on this central service. - """ - - def __init__( - self, - host: str = "localhost", # Made non-optional with default - port: int = 8088, # Made non-optional with default - ): - """ - Initializes the IsekCenterRegistry. - - :param host: The hostname or IP address of the Isek Center service. - Defaults to "localhost". - :type host: str - :param port: The port number on which the Isek Center service is listening. - Defaults to 8088. - :type port: int - """ - if not host: # Should not happen with default, but good practice - raise ValueError("Host for Isek Center cannot be empty.") - if not isinstance(port, int) or not (0 < port < 65536): - raise ValueError(f"Invalid port number for Isek Center: {port}") - - self.center_address: str = f"http://{host}:{port}" - # self.node_info: NodeInfo = {} # This instance variable seems to store the last registered node's info - # by this instance, which might be confusing if multiple nodes use - # the same registry instance. Consider if this state is necessary at instance level. - # For now, I'll assume it's for the node this client represents. - log.info( - f"IsekCenterRegistry initialized. Center address: {self.center_address}" - ) - - def _handle_response( - self, response: requests.Response, operation_name: str - ) -> Dict[str, Any]: - """ - Helper method to handle HTTP responses from the Isek Center. - - Checks for successful HTTP status codes and expected JSON structure. - - :param response: The `requests.Response` object. - :type response: requests.Response - :param operation_name: A string describing the operation (e.g., "register node"). - :type operation_name: str - :return: The parsed JSON response data if successful. - :rtype: typing.Dict[str, typing.Any] - :raises HTTPError: If the HTTP request itself failed (e.g., 4xx, 5xx status codes). - :raises ConnectionError: If there was a network problem. - :raises Timeout: If the request timed out. - :raises RuntimeError: If the Isek Center returns a non-200 'code' in its JSON response. - :raises ValueError: If the response content is not valid JSON. - """ - response.raise_for_status() # Raises HTTPError for bad responses (4XX or 5XX) - try: - response_json: Dict[str, Any] = response.json() - except json.JSONDecodeError as e: - log.error( - f"Failed to decode JSON response during {operation_name} " - f"from {response.url}. Response text: '{response.text[:200]}...'" - ) - raise ValueError( - f"Invalid JSON response received during {operation_name}." - ) from e - - if response_json.get("code") != 200: - error_message = response_json.get( - "message", "Unknown error from Isek Center." - ) - log.error( - f"{operation_name.capitalize()} failed at Isek Center. " - f"Code: {response_json.get('code')}, Message: {error_message}, " - f"Full response: {response_json}" - ) - raise RuntimeError( - f"{operation_name.capitalize()} failed at Isek Center: {error_message} (Code: {response_json.get('code')})" - ) - return response_json - - def register_node( - self, - node_id: str, - host: str, - port: int, - metadata: Optional[NodeMetadata] = None, - ) -> None: - """ - Registers a node with the Isek Center. - - Sends a POST request with node information to the center's `/isek_center/register` endpoint. - - :param node_id: The unique identifier for the node. - :type node_id: str - :param host: The hostname or IP address where the node can be reached. - :type host: str - :param port: The port number on which the node is listening. - :type port: int - :param metadata: Optional dictionary of key-value pairs providing additional - information about the node. Defaults to an empty dictionary. - :type metadata: typing.Optional[NodeMetadata] - :raises RuntimeError: If the Isek Center returns an error code in its response. - :raises requests.exceptions.RequestException: For network errors or HTTP error statuses. - """ - register_url = f"{self.center_address}/isek_center/register" - current_node_info: NodeInfo = { # Use a local variable for current operation - "node_id": node_id, - "host": host, - "port": port, - "metadata": metadata or {}, - } - # self.node_info = current_node_info # If self.node_info is intended to store this - - try: - log.debug( - f"Registering node '{node_id}' at {register_url} with data: {current_node_info}" - ) - response = requests.post( - url=register_url, json=current_node_info, timeout=10 - ) # Added timeout - response_data = self._handle_response( - response, f"register node '{node_id}'" - ) - # Assuming response_data might contain useful info, e.g., lease ID or confirmation details - log.info( - f"Node '{node_id}' registered successfully. " - f"Isek Center response: {response_data.get('message', 'OK')}" - ) - except RequestException as e: - log.error( - f"Failed to register node '{node_id}' due to a network/HTTP error: {e}", - exc_info=True, - ) - raise # Re-raise the requests exception - except (RuntimeError, ValueError) as e: # From _handle_response - log.error( - f"Failed to register node '{node_id}' due to Isek Center error: {e}", - exc_info=True, - ) - raise # Re-raise the application-level error - - def lease_refresh(self, node_id: str) -> None: - """ - Refreshes the lease for a registered node with the Isek Center. - - Sends a POST request with the `node_id` to the center's `/isek_center/renew` endpoint. - - :param node_id: The ID of the node whose lease needs to be refreshed. - :type node_id: str - :raises RuntimeError: If the Isek Center returns an error code in its response. - :raises requests.exceptions.RequestException: For network errors or HTTP error statuses. - """ - lease_refresh_url = f"{self.center_address}/isek_center/renew" - payload = {"node_id": node_id} - - try: - log.debug(f"Refreshing lease for node '{node_id}' at {lease_refresh_url}") - response = requests.post( - url=lease_refresh_url, json=payload, timeout=5 - ) # Added timeout - response_data = self._handle_response( - response, f"refresh lease for node '{node_id}'" - ) - log.debug( - f"Node '{node_id}' lease refreshed successfully. " - f"Isek Center response: {response_data.get('message', 'OK')}" - ) - except RequestException as e: - log.error( - f"Failed to refresh lease for node '{node_id}' due to a network/HTTP error: {e}", - exc_info=True, - ) - raise - except (RuntimeError, ValueError) as e: - log.error( - f"Failed to refresh lease for node '{node_id}' due to Isek Center error: {e}", - exc_info=True, - ) - raise - - def get_available_nodes(self) -> Dict[str, NodeInfo]: - """ - Retrieves information about all currently available nodes from the Isek Center. - - Sends a GET request to the center's `/isek_center/available_nodes` endpoint. - The expected response structure from Isek Center is a JSON object with a 'data' key, - which in turn has an 'available_nodes' key containing the dictionary of nodes. - - :return: A dictionary where keys are node IDs and values are dictionaries - containing the node information (host, port, metadata, etc.) - as provided by the Isek Center. - :rtype: typing.Dict[str, NodeInfo] - :raises RuntimeError: If the Isek Center returns an error code or an unexpected data structure. - :raises requests.exceptions.RequestException: For network errors or HTTP error statuses. - """ - available_nodes_url = f"{self.center_address}/isek_center/available_nodes" - try: - log.debug(f"Fetching available nodes from {available_nodes_url}") - response = requests.get( - url=available_nodes_url, timeout=10 - ) # Added timeout - response_data = self._handle_response(response, "get available nodes") - - nodes_data = response_data.get("data", {}).get("available_nodes") - if nodes_data is None or not isinstance(nodes_data, dict): - log.error( - "Isek Center response for available nodes is missing 'data.available_nodes' " - f"or it's not a dictionary. Response: {response_data}" - ) - raise RuntimeError( - "Invalid data structure for available nodes received from Isek Center." - ) - log.debug(f"Successfully fetched {len(nodes_data)} available nodes.") - return nodes_data # type: ignore # If linter complains about Dict[str, NodeInfo] vs Dict[str, Any] - except RequestException as e: - log.error( - f"Failed to get available nodes due to a network/HTTP error: {e}", - exc_info=True, - ) - raise - except (RuntimeError, ValueError) as e: - log.error( - f"Failed to get available nodes due to Isek Center error: {e}", - exc_info=True, - ) - raise - - def deregister_node(self, node_id: str) -> None: - """ - Deregisters a node from the Isek Center. - - Sends a POST request with the `node_id` to the center's `/isek_center/deregister` endpoint. - - :param node_id: The ID of the node to deregister. - :type node_id: str - :raises RuntimeError: If the Isek Center returns an error code in its response. - :raises requests.exceptions.RequestException: For network errors or HTTP error statuses. - """ - deregister_url = f"{self.center_address}/isek_center/deregister" - payload = {"node_id": node_id} - - try: - log.debug(f"Deregistering node '{node_id}' at {deregister_url}") - response = requests.post( - url=deregister_url, json=payload, timeout=10 - ) # Added timeout - response_data = self._handle_response( - response, f"deregister node '{node_id}'" - ) - log.info( - f"Node '{node_id}' deregistered successfully. " - f"Isek Center response: {response_data.get('message', 'OK')}" - ) - except RequestException as e: - log.error( - f"Failed to deregister node '{node_id}' due to a network/HTTP error: {e}", - exc_info=True, - ) - raise - except (RuntimeError, ValueError) as e: - log.error( - f"Failed to deregister node '{node_id}' due to Isek Center error: {e}", - exc_info=True, - ) - raise diff --git a/isek/node/node_v2.py b/isek/node/node_v2.py deleted file mode 100644 index 82b30ce..0000000 --- a/isek/node/node_v2.py +++ /dev/null @@ -1,185 +0,0 @@ -import threading -import uuid -from abc import ABC -from typing import Dict, Any, Optional -from isek.exceptions import NodeUnavailableError -from isek.node.default_registry import DefaultRegistry -from isek.node.registry import Registry -from isek.protocol.a2a_protocol import A2AProtocol -from isek.protocol.protocol import Protocol -from isek.adapter.base import Adapter -from isek.adapter.simple_adapter import SimpleAdapter -from isek.utils.log import log - -NodeDetails = Dict[str, Any] - - -class Node(ABC): - def __init__( - self, - host: str = "localhost", - port: int = 8080, - p2p: bool = False, - p2p_server_port: int = 9000, - node_id: Optional[str] = None, - protocol: Optional[Protocol] = None, - registry: Optional[Registry] = None, - adapter: Optional[Adapter] = None, - **kwargs: Any, # To absorb any extra arguments - ): - if not host: - raise ValueError("Node host cannot be empty.") - if not isinstance(port, int) or not (0 < port < 65536): - raise ValueError(f"Invalid port number for Node: {port}") - if not node_id: - node_id = uuid.uuid4().hex - - self.host: str = host - self.port: int = port - self.p2p: bool = p2p - self.p2p_server_port: int = p2p_server_port - self.node_id: str = node_id - self.all_nodes: Dict[str, NodeDetails] = {} - self.registry = registry or DefaultRegistry() - self.adapter = adapter or SimpleAdapter() - self.protocol = protocol or A2AProtocol( - host=self.host, - port=self.port, - adapter=self.adapter, - p2p=self.p2p, - p2p_server_port=self.p2p_server_port, - ) - - def send_message(self, receiver_node_id: str, message: str, retry_count: int = 3): - current_retry = 0 - while current_retry < retry_count: - try: - receiver_node_details = self.all_nodes.get(receiver_node_id) - if not receiver_node_details: - # Refresh nodes once if not found, in case cache is stale. - log.warning( - f"Receiver node '{receiver_node_id}' not found in local cache. Refreshing node list once." - ) - self.__refresh_nodes() - receiver_node_details = self.all_nodes.get(receiver_node_id) - if not receiver_node_details: - raise NodeUnavailableError( - receiver_node_id, - "Node not found in registry after refresh.", - ) - if self.p2p: - return self.protocol.send_p2p_message( - self.node_id, - receiver_node_details["metadata"]["p2p_address"], - message, - ) - return self.protocol.send_message( - self.node_id, receiver_node_details["metadata"]["url"], message - ) - except Exception as e: - log.exception( - f"Attempt {current_retry + 1}/{retry_count}: Unexpected error sending message to " - f"node '{receiver_node_id}'. Message: '{message}'. Error: {e}", - exc_info=True, - ) - - current_retry += 1 - - log.error( - f"Failed to send message to node '{receiver_node_id}' after {retry_count} retries." - ) - return f"Error: Message delivery to '{receiver_node_id}' failed after {retry_count} attempts." - - def build_server(self, daemon: bool = False) -> None: - if self.p2p: - self.protocol.bootstrap_p2p_extension() - log.info("The p2p service has been launched.") - if self.registry and self.adapter: - node_metadata = self.adapter.get_adapter_card().__dict__ - node_metadata["url"] = f"http://{self.host}:{self.port}" - node_metadata["peer_id"] = self.protocol.peer_id - node_metadata["p2p_address"] = self.protocol.p2p_address - self.registry.register_node( - node_id=self.node_id, - host=self.host, - port=self.port, - metadata=node_metadata, - ) - self.__bootstrap_heartbeat() # Starts the recurring heartbeat - - if self.p2p: - if not self.protocol.peer_id or not self.protocol.p2p_address: - raise RuntimeError("p2p server not started, please check.") - - if not daemon: - self.protocol.bootstrap_server() - else: - server_thread = threading.Thread( - target=self.protocol.bootstrap_server, daemon=True - ) - server_thread.start() - - def __bootstrap_heartbeat(self) -> None: - """ - Manages the node's heartbeat to the registry. - - This method performs two main actions: - 1. Refreshes the node's lease with the registry to keep it active. - 2. Refreshes the local cache of available nodes (`self.all_nodes`). - - It then schedules itself to run again after a fixed interval (5 seconds). - """ - try: - self.registry.lease_refresh(self.node_id) - log.debug(f"Node '{self.node_id}' lease refreshed successfully.") - except Exception as e: - log.warning( - f"Failed to refresh lease for node '{self.node_id}': {e}. " - "Node might be deregistered if this persists.", - exc_info=True, - ) - # Depending on severity, could attempt re-registration or shutdown. - - try: - self.__refresh_nodes() - except Exception as e: - log.warning( - f"Failed to refresh node list for '{self.node_id}': {e}", exc_info=True - ) - - # Schedule next heartbeat - # Ensure timer is managed properly if the node is shut down - timer = threading.Timer(5, self.__bootstrap_heartbeat) - timer.daemon = True # Allows main program to exit even if timer is active - timer.start() - log.debug(f"Node '{self.node_id}' heartbeat scheduled.") - - def __refresh_nodes(self) -> None: - """ - Refreshes the local cache of available nodes (`self.all_nodes`) - by querying the service registry. - - The commented-out section suggests plans for integrating a vector-based - node index (e.g., using Faiss) for more advanced node discovery. - """ - try: - current_available_nodes = self.registry.get_available_nodes() - # Simple update: replace the old list. - # More sophisticated updates might involve diffing for logging or specific actions. - if self.all_nodes != current_available_nodes: # Basic check for changes - # logger.info(f"Node list updated for '{self.node_id}'. " - # f"Previous count: {len(self.all_nodes)}, New count: {len(current_available_nodes)}.") - self.all_nodes = current_available_nodes - else: - log.debug( - f"Node list for '{self.node_id}' remains unchanged. Count: {len(self.all_nodes)}." - ) - except Exception as e: - # This exception is from self.registry.get_available_nodes() - log.error( - f"Failed to retrieve available nodes from registry: {e}", exc_info=True - ) - # self.all_nodes might become stale if this fails repeatedly. - - def stop_server(self) -> None: - self.protocol.stop_server() diff --git a/isek/node/registry.py b/isek/node/registry.py deleted file mode 100644 index 60a3b13..0000000 --- a/isek/node/registry.py +++ /dev/null @@ -1,26 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Optional, Dict - - -class Registry(ABC): - @abstractmethod - def register_node( - self, - node_id: str, - host: str, - port: int, - metadata: Optional[Dict[str, str]] = None, - ): - pass - - @abstractmethod - def get_available_nodes(self) -> dict: - pass - - @abstractmethod - def deregister_node(self, node_id: str): - pass - - @abstractmethod - def lease_refresh(self, node_id: str): - pass diff --git a/isek/protocol/a2a_protocol.py b/isek/protocol/a2a_protocol.py deleted file mode 100644 index 71684ad..0000000 --- a/isek/protocol/a2a_protocol.py +++ /dev/null @@ -1,230 +0,0 @@ -import asyncio -from typing import Any -from typing import Optional -from uuid import uuid4 -import threading -import time -import os -import subprocess -import json -import atexit -import urllib - -import httpx -import uvicorn -from a2a.client import A2AClient -from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.apps import A2AStarletteApplication -from a2a.server.apps.jsonrpc import JSONRPCApplication -from a2a.server.events import EventQueue -from a2a.server.request_handlers import DefaultRequestHandler -from a2a.server.tasks import InMemoryTaskStore -from a2a.types import AgentCard, AgentCapabilities -from a2a.types import ( - MessageSendParams, - SendMessageRequest, -) -from a2a.utils import new_agent_text_message - -from isek.protocol.protocol import Protocol -from isek.adapter.base import Adapter -from isek.adapter.simple_adapter import SimpleAdapter -from isek.utils.log import log - - -class DefaultAgentExecutor(AgentExecutor): - def __init__(self, url: str, adapter: Adapter): - self.url = url - self.adapter = adapter - - def get_a2a_agent_card(self) -> AgentCard: - adapter_card = self.adapter.get_adapter_card() - return AgentCard( - name=adapter_card.name, - description=f"bio:{adapter_card.bio}\nlore:{adapter_card.lore}\nknowledge:{adapter_card.knowledge}", - url=self.url, - version="1.0.0", - defaultInputModes=["text"], - defaultOutputModes=["text"], - capabilities=AgentCapabilities(streaming=True), - skills=[], - # supportsAuthenticatedExtendedCard=True, - ) - - async def execute( - self, - context: RequestContext, - event_queue: EventQueue, - ) -> None: - prompt = context.get_user_input() - result = self.adapter.run(prompt=prompt) - await event_queue.enqueue_event(new_agent_text_message(result)) - - async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: - raise Exception("cancel not supported") - - -def build_send_message_request(sender_node_id, message): - send_message_payload: dict[str, Any] = { - "message": { - "role": "user", - "parts": [{"kind": "text", "text": message}], - "messageId": uuid4().hex, - "metadata": {"sender_node_id": sender_node_id}, - }, - "metadata": {"sender_node_id": sender_node_id}, - } - return SendMessageRequest( - id=str(uuid4()), params=MessageSendParams(**send_message_payload) - ) - - -class A2AProtocol(Protocol): - def __init__( - self, - a2a_application: Optional[JSONRPCApplication] = None, - host: str = "localhost", - port: int = 8080, - p2p: bool = True, - p2p_server_port: int = 9000, - adapter: Optional[Adapter] = None, - **kwargs: Any, - ): - super().__init__( - host=host, - port=port, - adapter=adapter, - p2p=p2p, - p2p_server_port=p2p_server_port, - **kwargs, - ) - self.adapter = adapter or SimpleAdapter() - self.peer_id = None - self.p2p_address = None - if a2a_application: - self.url = a2a_application.agent_card.url - self.a2a_application = a2a_application - else: - self.url = f"http://{host}:{port}/" - self.a2a_application = self.build_a2a_application() - - def bootstrap_server(self): - uvicorn.run(self.a2a_application.build(), host="0.0.0.0", port=self.port) - - def bootstrap_p2p_extension(self): - if self.p2p and self.p2p_server_port: - self.__bootstrap_p2p_server() - # self.__bootstrap_heartbeat() - - def __bootstrap_p2p_server(self): - def stream_output(stream): - for line in iter(stream.readline, ""): - log.debug(line) - - dirc = os.path.dirname(__file__) - # parent = os.path.abspath(os.path.join(dirc, "..", "..")) - p2p_file_path = os.path.join(dirc, "p2p", "p2p_server.js") - process = subprocess.Popen( - [ - "node", - p2p_file_path, - f"--port={self.p2p_server_port}", - f"--agent_port={self.port}", - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - ) - - def cleanup(): - if process and process.poll() is None: - process.terminate() - log.debug(f"p2p_server[port:{self.p2p_server_port}] process terminated") - - atexit.register(cleanup) - thread = threading.Thread( - target=stream_output, args=(process.stdout,), daemon=True - ) - thread.start() - while True: - if process.poll() is not None: - raise RuntimeError( - f"p2p_server process exited unexpectedly with code {process.returncode}" - ) - - p2p_context = self.__load_p2p_context() - if p2p_context and self.peer_id and self.p2p_address: - log.debug(f"The p2p service has been completed: {p2p_context}") - break - time.sleep(1) - - def __load_p2p_context(self): - try: - response = httpx.get(f"http://localhost:{self.p2p_server_port}/p2p_context") - response_body = json.loads(response.content) - self.peer_id = response_body["peer_id"] - self.p2p_address = response_body["p2p_address"] - log.debug(f"__load_p2p_context response[{response_body}]") - return response_body - except Exception: - log.exception("Load p2p server context error.") - return None - - # def __bootstrap_heartbeat(self) -> None: - # self.__load_p2p_context() - # timer = threading.Timer(5, self.__bootstrap_heartbeat) - # timer.daemon = True - # timer.start() - - def stop_server(self) -> None: - pass - - def send_p2p_message(self, sender_node_id, p2p_address, message): - request = build_send_message_request(sender_node_id, message) - request_body = request.model_dump(mode="json", exclude_none=True) - response = httpx.post( - url=f"http://localhost:{self.p2p_server_port}/call_peer?p2p_address={urllib.parse.quote(p2p_address)}", - json=request_body, - headers={"Content-Type": "application/json"}, - timeout=60, - ) - response_body = json.loads(response.content) - return response_body["result"]["parts"][0]["text"] - - def send_message(self, sender_node_id, target_address, message): - httpx_client = httpx.AsyncClient(timeout=60) - client = A2AClient(httpx_client=httpx_client, url=target_address) - request = build_send_message_request(sender_node_id, message) - response = asyncio.run(client.send_message(request)) - return response.model_dump(mode="json", exclude_none=True)["result"]["parts"][ - 0 - ]["text"] - - def build_a2a_application(self) -> JSONRPCApplication: - if not self.adapter or not isinstance(self.adapter, Adapter): - raise ValueError("A Adapter must be provided to the A2AProtocol.") - else: - agent_executor = DefaultAgentExecutor(self.url, self.adapter) - request_handler = DefaultRequestHandler( - agent_executor=agent_executor, - task_store=InMemoryTaskStore(), - ) - - return A2AStarletteApplication( - agent_card=agent_executor.get_a2a_agent_card(), - http_handler=request_handler, - ) - - def default_a2a_application(self): - if not self.adapter: - raise ValueError("A Adapter must be provided to the A2AProtocol.") - agent_executor = DefaultAgentExecutor(self.url, self.adapter) - request_handler = DefaultRequestHandler( - agent_executor=agent_executor, - task_store=InMemoryTaskStore(), - ) - - return A2AStarletteApplication( - agent_card=agent_executor.get_a2a_agent_card(), http_handler=request_handler - ) diff --git a/isek/protocol/protocol.py b/isek/protocol/protocol.py deleted file mode 100644 index d6e835e..0000000 --- a/isek/protocol/protocol.py +++ /dev/null @@ -1,41 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, Optional - -from isek.adapter.base import Adapter - - -class Protocol(ABC): - def __init__( - self, - host: str = "localhost", - port: int = 8080, - p2p: bool = False, - p2p_server_port: int = 9000, - adapter: Optional[Adapter] = None, - **kwargs: Any, - ): - self.adapter = adapter - self.host = host or "localhost" - self.port = port or 8080 - self.p2p = p2p or False - self.p2p_server_port = p2p_server_port or 9000 - - @abstractmethod - def bootstrap_server(self): - pass - - @abstractmethod - def bootstrap_p2p_extension(self): - pass - - @abstractmethod - def stop_server(self) -> None: - pass - - @abstractmethod - def send_message(self, sender_node_id, target_address, message): - pass - - @abstractmethod - def send_p2p_message(self, sender_node_id, p2p_address, message): - pass diff --git a/isek/team/__init__.py b/isek/team/__init__.py deleted file mode 100644 index a2c8525..0000000 --- a/isek/team/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .isek_team import IsekTeam - -__all__ = [ - "IsekTeam", -] - -__version__ = "1.0.0" diff --git a/isek/team/isek_team.py b/isek/team/isek_team.py deleted file mode 100644 index cb53179..0000000 --- a/isek/team/isek_team.py +++ /dev/null @@ -1,480 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Callable, List, Optional, Union, Any, Dict, Sequence, Literal -from uuid import uuid4 - -from isek.agent.isek_agent import IsekAgent -from isek.memory.memory import Memory, UserMemory -from isek.models.base import Model, SimpleMessage -from isek.tools.toolkit import Toolkit -from isek.utils.log import log -from isek.utils.print_utils import print_response - - -@dataclass -class IsekTeam: - """Ultra-simplified Team class that coordinates multiple agents.""" - - # List of team members (agents or other teams) - required field - members: List[Union[IsekAgent, "IsekTeam"]] - # Team name - name: Optional[str] = None - # Team UUID (autogenerated if not set) - team_id: Optional[str] = None - # Model for this Team (used for coordination) - model: Optional[Model] = None - # Team memory - memory: Optional[Memory] = None - # Tools provided to the Team - tools: Optional[List[Toolkit]] = None - # A description of the Team - description: Optional[str] = None - # Success criteria for the task - success_criteria: Optional[str] = None - # List of instructions for the team - instructions: Optional[Union[str, List[str], Callable]] = None - # Enable debug logs - debug_mode: bool = False - # Coordination mode: 'coordinate' (default), 'route', 'collaborate', or 'sequential' - mode: Literal["coordinate", "route", "collaborate", "sequential"] = "coordinate" - - def __post_init__(self): - """Initialize the team after creation.""" - # Set team ID if not provided - if self.team_id is None: - self.team_id = str(uuid4()) - - # Set debug mode - if self.debug_mode: - log.debug( - f"Team initialized: {self.name or 'Unnamed'} (ID: {self.team_id})" - ) - - def run( - self, - message: str, - user_id: str = "default", - session_id: Optional[str] = None, - messages: Optional[List[Union[Dict, Any]]] = None, - audio: Optional[Sequence[Any]] = None, - images: Optional[Sequence[Any]] = None, - videos: Optional[Sequence[Any]] = None, - files: Optional[Sequence[Any]] = None, - stream: Optional[bool] = None, - stream_intermediate_steps: bool = False, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> str: - """Execute the team's main functionality with the given message.""" - return self.run_with_context(message, user_id, session_id) - - def print_response(self, *args, **kwargs): - """ - Proxy to the shared print_response utility, passing self.run as run_func. - """ - return print_response(self.run, *args, **kwargs) - - def run_with_context( - self, message: str, user_id: str = "default", session_id: Optional[str] = None - ) -> str: - """Run the team with a message and return the response.""" - if not self.members: - raise ValueError("Team must have at least one member") - - # Generate session ID if not provided - if session_id is None: - session_id = str(uuid4()) - - # Handle single agent case - if len(self.members) == 1 and isinstance(self.members[0], IsekAgent): - return self.members[0].run(message, user_id, session_id) - - # Handle team coordination - if self.mode == "coordinate": - return self._coordinate_response(message, user_id, session_id) - elif self.mode == "route": - return self._route_response(message, user_id, session_id) - elif self.mode == "collaborate": - return self._collaborate_response(message, user_id, session_id) - elif self.mode == "sequential": - return self._sequential_response(message, user_id, session_id) - else: - raise ValueError(f"Unknown coordination mode: {self.mode}") - - def _coordinate_response(self, message: str, user_id: str, session_id: str) -> str: - """Coordinate multiple agents to generate a response.""" - if self.model is None: - raise ValueError("Model is required for team coordination") - - # Create system message for coordination - system_message = self._build_coordination_message(message) - - # Get relevant memories if memory is available - memory_context = self._get_memory_context(user_id) - - # Conversation history - messages = [] - if system_message: - messages.append(SimpleMessage(role="system", content=system_message)) - if memory_context: - messages.append( - SimpleMessage( - role="system", content=f"Previous context:\n{memory_context}" - ) - ) - messages.append(SimpleMessage(role="user", content=message)) - - # Call the model for coordination - response = self.model.response(messages=messages) - response_content = response.content or "No response generated" - - # Store in memory if available - if self.memory: - self._store_conversation(user_id, session_id, message, response_content) - - if self.debug_mode: - log.debug(f"Team coordination response: {response_content}") - - return response_content - - def _route_response(self, message: str, user_id: str, session_id: str) -> str: - """Route the task to the most appropriate team member.""" - if self.model is None: - raise ValueError("Model is required for team routing") - - # Create system message for routing - system_message = self._build_routing_message(message) - - # Get relevant memories if memory is available - memory_context = self._get_memory_context(user_id) - - # Conversation history - messages = [] - if system_message: - messages.append(SimpleMessage(role="system", content=system_message)) - if memory_context: - messages.append( - SimpleMessage( - role="system", content=f"Previous context:\n{memory_context}" - ) - ) - messages.append(SimpleMessage(role="user", content=message)) - - # Call the model for routing - response = self.model.response(messages=messages) - response_content = response.content or "No response generated" - - # Store in memory if available - if self.memory: - self._store_conversation(user_id, session_id, message, response_content) - - if self.debug_mode: - log.debug(f"Team routing response: {response_content}") - - return response_content - - def _collaborate_response(self, message: str, user_id: str, session_id: str) -> str: - """Collaborate with all team members to generate a response.""" - if self.model is None: - raise ValueError("Model is required for team collaboration") - - # Create system message for collaboration - system_message = self._build_collaboration_message(message) - - # Get relevant memories if memory is available - memory_context = self._get_memory_context(user_id) - - # Conversation history - messages = [] - if system_message: - messages.append(SimpleMessage(role="system", content=system_message)) - if memory_context: - messages.append( - SimpleMessage( - role="system", content=f"Previous context:\n{memory_context}" - ) - ) - messages.append(SimpleMessage(role="user", content=message)) - - # Call the model for collaboration - response = self.model.response(messages=messages) - response_content = response.content or "No response generated" - - # Store in memory if available - if self.memory: - self._store_conversation(user_id, session_id, message, response_content) - - if self.debug_mode: - log.debug(f"Team collaboration response: {response_content}") - - return response_content - - def _sequential_response(self, message: str, user_id: str, session_id: str) -> str: - """Run agents sequentially, passing output to next agent.""" - current_message = message - - for member in self.members: - if isinstance(member, IsekAgent): - current_message = member.run(current_message, user_id, session_id) - elif isinstance(member, IsekTeam): - current_message = member.run_with_context( - current_message, user_id, session_id - ) - - return current_message - - def _build_coordination_message(self, user_message: str) -> str: - """Build the coordination message for the team.""" - parts = [] - - # Team description - if self.description: - parts.append(f"Team Description: {self.description}") - else: - parts.append( - "You are coordinating a team of agents to respond to user requests." - ) - - # Team members info - parts.append("\nTeam Members:") - for i, member in enumerate(self.members, 1): - if isinstance(member, IsekAgent): - member_desc = member.description or f"Agent {member.name or i}" - parts.append(f"{i}. {member.name or f'Agent {i}'}: {member_desc}") - elif isinstance(member, IsekTeam): - member_desc = member.description or f"Team {member.name or i}" - parts.append(f"{i}. {member.name or f'Team {i}'}: {member_desc}") - - # Success criteria - if self.success_criteria: - parts.append(f"\nSuccess Criteria: {self.success_criteria}") - - # Instructions - if self.instructions: - if isinstance(self.instructions, str): - parts.append(f"\nInstructions: {self.instructions}") - elif isinstance(self.instructions, list): - parts.append("\nInstructions:") - for instruction in self.instructions: - parts.append(f"- {instruction}") - elif callable(self.instructions): - parts.append(f"\nInstructions: {self.instructions()}") - - # Coordination instructions - parts.append( - "\nCoordination Instructions:" - "\n- Analyze the user's request" - "\n- Determine which team members should be involved" - "\n- Provide a comprehensive response that leverages the team's capabilities" - "\n- If specific tools or calculations are needed, mention which agent would handle them" - ) - - return "\n".join(parts) - - def _build_routing_message(self, user_message: str) -> str: - """Build the routing message for the team.""" - parts = [] - - # Team description - if self.description: - parts.append(f"Team Description: {self.description}") - else: - parts.append("You are routing tasks to the most appropriate team members.") - - # Team members info - parts.append("\nTeam Members:") - for i, member in enumerate(self.members, 1): - if isinstance(member, IsekAgent): - member_desc = member.description or f"Agent {member.name or i}" - parts.append(f"{i}. {member.name or f'Agent {i}'}: {member_desc}") - elif isinstance(member, IsekTeam): - member_desc = member.description or f"Team {member.name or i}" - parts.append(f"{i}. {member.name or f'Team {i}'}: {member_desc}") - - # Success criteria - if self.success_criteria: - parts.append(f"\nSuccess Criteria: {self.success_criteria}") - - # Instructions - if self.instructions: - if isinstance(self.instructions, str): - parts.append(f"\nInstructions: {self.instructions}") - elif isinstance(self.instructions, list): - parts.append("\nInstructions:") - for instruction in self.instructions: - parts.append(f"- {instruction}") - elif callable(self.instructions): - parts.append(f"\nInstructions: {self.instructions()}") - - # Routing instructions - parts.append( - "\nRouting Instructions:" - "\n- Analyze the user's request" - "\n- Determine which team member is most suitable for the task" - "\n- Route the task to the appropriate member" - "\n- Provide a response that leverages the team's capabilities" - "\n- If specific tools or calculations are needed, mention which agent would handle them" - ) - - return "\n".join(parts) - - def _build_collaboration_message(self, user_message: str) -> str: - """Build the collaboration message for the team.""" - parts = [] - - # Team description - if self.description: - parts.append(f"Team Description: {self.description}") - else: - parts.append( - "You are collaborating with all team members to respond to user requests." - ) - - # Team members info - parts.append("\nTeam Members:") - for i, member in enumerate(self.members, 1): - if isinstance(member, IsekAgent): - member_desc = member.description or f"Agent {member.name or i}" - parts.append(f"{i}. {member.name or f'Agent {i}'}: {member_desc}") - elif isinstance(member, IsekTeam): - member_desc = member.description or f"Team {member.name or i}" - parts.append(f"{i}. {member.name or f'Team {i}'}: {member_desc}") - - # Success criteria - if self.success_criteria: - parts.append(f"\nSuccess Criteria: {self.success_criteria}") - - # Instructions - if self.instructions: - if isinstance(self.instructions, str): - parts.append(f"\nInstructions: {self.instructions}") - elif isinstance(self.instructions, list): - parts.append("\nInstructions:") - for instruction in self.instructions: - parts.append(f"- {instruction}") - elif callable(self.instructions): - parts.append(f"\nInstructions: {self.instructions()}") - - # Collaboration instructions - parts.append( - "\nCollaboration Instructions:" - "\n- Work together with all team members" - "\n- Coordinate efforts to provide comprehensive responses" - "\n- Leverage each member's unique capabilities" - "\n- Provide a unified response that combines all members' expertise" - "\n- If specific tools or calculations are needed, coordinate with the appropriate agents" - ) - - return "\n".join(parts) - - def _get_memory_context(self, user_id: str) -> Optional[str]: - """Get relevant memory context for the user.""" - if not self.memory: - return None - - memories = self.memory.get_user_memories(user_id) - if not memories: - return None - - memory_texts = [] - for memory in memories: - memory_texts.append(f"- {memory.memory}") - - return "Previous interactions:\n" + "\n".join(memory_texts) - - def _store_conversation( - self, user_id: str, session_id: str, user_message: str, team_response: str - ) -> None: - """Store the conversation in memory.""" - if not self.memory: - return - - # Store user memory - user_memory = UserMemory( - memory=f"User: {user_message}\nTeam: {team_response}", - topics=["conversation"], - ) - self.memory.add_user_memory(user_memory, user_id) - - # Store run - run_data = { - "user_message": user_message, - "team_response": team_response, - "timestamp": str(uuid4()), - } - self.memory.add_run(session_id, run_data) - - if self.debug_mode: - log.debug( - f"Stored conversation in memory for user {user_id}, session {session_id}" - ) - - def get_agent_config(self) -> dict: - """Get agent configuration for A2A protocol.""" - return { - "name": self.name or "ISEK Team", - "description": self.description or "A team of ISEK agents", - "instructions": self.instructions or "Coordinate team members effectively", - "success_criteria": self.success_criteria - or "Complete the task successfully", - "lore": "This is a team of agents.", - "knowledge": "", # Knowledge is specific to agents, so leave blank for team. - } - - def get_available_tools(self) -> List[dict]: - """Get all available tools from team members.""" - tools = [] - for member in self.members: - if isinstance(member, IsekAgent): - if member.tools: - for toolkit in member.tools: - for func in toolkit.functions.values(): - tools.append( - {"type": "function", "function": func.to_dict()} - ) - elif isinstance(member, IsekTeam): - tools.extend(member.get_available_tools()) - return tools - - def get_member_names(self) -> List[str]: - """Get names of all team members.""" - names = [] - for member in self.members: - if isinstance(member, IsekAgent): - names.append(member.name or "Unnamed Agent") - elif isinstance(member, IsekTeam): - names.append(member.name or "Unnamed Team") - return names - - def get_member_by_name(self, name: str) -> Optional[Union[IsekAgent, "IsekTeam"]]: - """Get a team member by name.""" - for member in self.members: - if isinstance(member, IsekAgent): - if member.name == name: - return member - elif isinstance(member, IsekTeam): - if member.name == name: - return member - return None - - def add_member(self, member: Union[IsekAgent, "IsekTeam"]) -> None: - """Add a new member to the team.""" - self.members.append(member) - if self.debug_mode: - member_name = member.name or "Unnamed" - log.debug(f"Added member '{member_name}' to team '{self.name}'") - - def remove_member(self, member: Union[IsekAgent, "IsekTeam"]) -> bool: - """Remove a member from the team.""" - try: - self.members.remove(member) - if self.debug_mode: - member_name = member.name or "Unnamed" - log.debug(f"Removed member '{member_name}' from team '{self.name}'") - return True - except ValueError: - return False - - def __repr__(self) -> str: - return f"Team(name='{self.name}', id='{self.team_id}', members={len(self.members)})" diff --git a/isek/team/team.py b/isek/team/team.py deleted file mode 100644 index ddc8f3b..0000000 --- a/isek/team/team.py +++ /dev/null @@ -1,8602 +0,0 @@ -import asyncio -import json -import threading -from collections import ChainMap, defaultdict, deque -from dataclasses import asdict, dataclass, replace -from os import getenv -from textwrap import dedent -from typing import ( - Any, - AsyncIterator, - Callable, - Dict, - Iterator, - List, - Literal, - Optional, - Sequence, - Set, - Tuple, - Type, - Union, - cast, - get_args, - overload, -) -from uuid import uuid4 - -from pydantic import BaseModel - -from agno.agent import Agent -from agno.agent.metrics import SessionMetrics -from agno.exceptions import ModelProviderError, RunCancelledException -from agno.knowledge.agent import AgentKnowledge -from agno.media import ( - Audio, - AudioArtifact, - AudioResponse, - File, - Image, - ImageArtifact, - Video, - VideoArtifact, -) -from agno.memory.agent import AgentMemory -from agno.memory.team import TeamMemory, TeamRun -from agno.memory.v2.memory import Memory, SessionSummary -from agno.models.base import Model -from agno.models.message import Citations, Message, MessageReferences -from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecution -from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps -from agno.run.base import RunResponseExtraData, RunStatus -from agno.run.messages import RunMessages -from agno.run.response import RunResponse, RunResponseEvent -from agno.run.team import ( - TeamRunEvent, - TeamRunResponse, - TeamRunResponseEvent, - ToolCallCompletedEvent, -) -from agno.storage.base import Storage -from agno.storage.session.team import TeamSession -from agno.tools.function import Function -from agno.tools.toolkit import Toolkit -from agno.utils.events import ( - create_team_memory_update_completed_event, - create_team_memory_update_started_event, - create_team_reasoning_completed_event, - create_team_reasoning_started_event, - create_team_reasoning_step_event, - create_team_run_response_cancelled_event, - create_team_run_response_completed_event, - create_team_run_response_content_event, - create_team_run_response_error_event, - create_team_run_response_started_event, - create_team_tool_call_completed_event, - create_team_tool_call_started_event, -) -from agno.utils.log import ( - log_debug, - log_error, - log_exception, - log_info, - log_warning, - set_log_level_to_debug, - set_log_level_to_info, - use_agent_logger, - use_team_logger, -) -from agno.utils.merge_dict import merge_dictionaries -from agno.utils.message import get_text_from_message -from agno.utils.response import ( - check_if_run_cancelled, - create_panel, - escape_markdown_tags, - format_tool_calls, - update_run_response_with_reasoning, -) -from agno.utils.safe_formatter import SafeFormatter -from agno.utils.string import is_valid_uuid, parse_response_model_str, url_safe_string -from agno.utils.timer import Timer - - -@dataclass(init=False) -class Team: - """ - A class representing a team of agents. - """ - - members: List[Union[Agent, "Team"]] - - mode: Literal["route", "coordinate", "collaborate"] = "coordinate" - - # Model for this Team - model: Optional[Model] = None - - # --- Team settings --- - # Name of the team - name: Optional[str] = None - # Team UUID (autogenerated if not set) - team_id: Optional[str] = None - # If this team is part of a team itself, this is the role of the team - parent_team_id: Optional[str] = None - # The workflow this team belongs to - workflow_id: Optional[str] = None - role: Optional[str] = None - - # --- User settings --- - # ID of the user interacting with this team - user_id: Optional[str] = None - - # --- Session settings --- - # Team Session UUID (autogenerated if not set) - session_id: Optional[str] = None - # In the case where the team is a member of a team itself - team_session_id: Optional[str] = None - # Session name - session_name: Optional[str] = None - # Session state (stored in the database to persist across runs) - session_state: Optional[Dict[str, Any]] = None - - # Team session state (shared between team leaders and team members) - team_session_state: Optional[Dict[str, Any]] = None - # If True, add the session state variables in the user and system messages - add_state_in_messages: bool = False - - # --- System message settings --- - # A description of the Team that is added to the start of the system message. - description: Optional[str] = None - # List of instructions for the team. - instructions: Optional[Union[str, List[str], Callable]] = None - # Provide the expected output from the Team. - expected_output: Optional[str] = None - # Additional context added to the end of the system message. - additional_context: Optional[str] = None - # If markdown=true, add instructions to format the output using markdown - markdown: bool = False - # If True, add the current datetime to the instructions to give the team a sense of time - # This allows for relative times like "tomorrow" to be used in the prompt - add_datetime_to_instructions: bool = False - # If True, add the current location to the instructions to give the team a sense of location - add_location_to_instructions: bool = False - # If True, add the tools available to team members to the system message - add_member_tools_to_system_message: bool = True - - # Provide the system message as a string or function - system_message: Optional[Union[str, Callable, Message]] = None - # Role for the system message - system_message_role: str = "system" - - # --- Success criteria --- - # Define the success criteria for the team - success_criteria: Optional[str] = None - - # --- User provided context --- - # User provided context - context: Optional[Dict[str, Any]] = None - # If True, add the context to the user prompt - add_context: bool = False - - # --- Agent Knowledge --- - knowledge: Optional[AgentKnowledge] = None - # Add knowledge_filters to the Agent class attributes - knowledge_filters: Optional[Dict[str, Any]] = None - # Let the agent choose the knowledge filters - enable_agentic_knowledge_filters: Optional[bool] = False - - # If True, add references to the user prompt - add_references: bool = False - # Retrieval function to get references - # This function, if provided, is used instead of the default search_knowledge function - # Signature: - # def retriever(team: Team, query: str, num_documents: Optional[int], **kwargs) -> Optional[list[dict]]: - # ... - retriever: Optional[Callable[..., Optional[List[Union[Dict, str]]]]] = None - references_format: Literal["json", "yaml"] = "json" - - # --- Tools --- - # If True, enable the team agent to update the team context and automatically send the team context to the members - enable_agentic_context: bool = False - # If True, send all previous member interactions to members - share_member_interactions: bool = False - # If True, add a tool to get information about the team members - get_member_information_tool: bool = False - # Add a tool to search the knowledge base (aka Agentic RAG) - # Only added if knowledge is provided. - search_knowledge: bool = True - - # If True, read the team history - read_team_history: bool = False - - # --- Team Tools --- - # A list of tools provided to the Model. - # Tools are functions the model may generate JSON inputs for. - tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None - # Show tool calls in Team response. This sets the default for the team. - show_tool_calls: bool = True - # Controls which (if any) tool is called by the team model. - # "none" means the model will not call a tool and instead generates a message. - # "auto" means the model can pick between generating a message or calling a tool. - # Specifying a particular function via {"type: "function", "function": {"name": "my_function"}} - # forces the model to call that tool. - # "none" is the default when no tools are present. "auto" is the default if tools are present. - tool_choice: Optional[Union[str, Dict[str, Any]]] = None - # Maximum number of tool calls allowed. - tool_call_limit: Optional[int] = None - # A list of hooks to be called before and after the tool call - tool_hooks: Optional[List[Callable]] = None - - # --- Structured output --- - # Response model for the team response - response_model: Optional[Type[BaseModel]] = None - # If `response_model` is set, sets the response mode of the model, i.e. if the model should explicitly respond with a JSON object instead of a Pydantic model - use_json_mode: bool = False - # If True, parse the response - parse_response: bool = True - - # --- History --- - # Memory for the team - memory: Optional[Union[TeamMemory, Memory]] = None - # Enable the agent to manage memories of the user - enable_agentic_memory: bool = False - # If True, the agent creates/updates user memories at the end of runs - enable_user_memories: bool = False - # If True, the agent adds a reference to the user memories in the response - add_memory_references: Optional[bool] = None - # If True, the agent creates/updates session summaries at the end of runs - enable_session_summaries: bool = False - # If True, the agent adds a reference to the session summaries in the response - add_session_summary_references: Optional[bool] = None - - # --- Team History --- - # If True, enable the team history (Deprecated in favor of add_history_to_messages) - enable_team_history: bool = False - # add_history_to_messages=true adds messages from the chat history to the messages list sent to the Model. - add_history_to_messages: bool = False - # Deprecated in favor of num_history_runs: Number of interactions from history - num_of_interactions_from_history: Optional[int] = None - # Number of historical runs to include in the messages - num_history_runs: int = 3 - - # --- Team Storage --- - storage: Optional[Storage] = None - # Extra data stored with this team - extra_data: Optional[Dict[str, Any]] = None - - # --- Team Reasoning --- - reasoning: bool = False - reasoning_model: Optional[Model] = None - reasoning_agent: Optional[Agent] = None - reasoning_min_steps: int = 1 - reasoning_max_steps: int = 10 - - # --- Team Streaming --- - # Stream the response from the Team - stream: Optional[bool] = None - # Stream the intermediate steps from the Team - stream_intermediate_steps: bool = False - - # Optional app ID. Indicates this team is part of an app. - app_id: Optional[str] = None - # --- Debug & Monitoring --- - # Enable debug logs - debug_mode: bool = False - # Enable member logs - Sets the debug_mode for team and members - show_members_responses: bool = False - # monitoring=True logs Team information to agno.com for monitoring - monitoring: bool = False - # telemetry=True logs minimal telemetry for analytics - # This helps us improve the Teams implementation and provide better support - telemetry: bool = True - - def __init__( - self, - members: List[Union[Agent, "Team"]], - mode: Literal["route", "coordinate", "collaborate"] = "coordinate", - model: Optional[Model] = None, - name: Optional[str] = None, - team_id: Optional[str] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - session_name: Optional[str] = None, - session_state: Optional[Dict[str, Any]] = None, - team_session_state: Optional[Dict[str, Any]] = None, - add_state_in_messages: bool = False, - description: Optional[str] = None, - instructions: Optional[Union[str, List[str], Callable]] = None, - expected_output: Optional[str] = None, - additional_context: Optional[str] = None, - success_criteria: Optional[str] = None, - markdown: bool = False, - add_datetime_to_instructions: bool = False, - add_location_to_instructions: bool = False, - add_member_tools_to_system_message: bool = True, - system_message: Optional[Union[str, Callable, Message]] = None, - system_message_role: str = "system", - context: Optional[Dict[str, Any]] = None, - add_context: bool = False, - knowledge: Optional[AgentKnowledge] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - add_references: bool = False, - enable_agentic_knowledge_filters: Optional[bool] = False, - retriever: Optional[Callable[..., Optional[List[Union[Dict, str]]]]] = None, - references_format: Literal["json", "yaml"] = "json", - enable_agentic_context: bool = False, - share_member_interactions: bool = False, - get_member_information_tool: bool = False, - search_knowledge: bool = True, - read_team_history: bool = False, - tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None, - show_tool_calls: bool = True, - tool_call_limit: Optional[int] = None, - tool_choice: Optional[Union[str, Dict[str, Any]]] = None, - tool_hooks: Optional[List[Callable]] = None, - response_model: Optional[Type[BaseModel]] = None, - use_json_mode: bool = False, - parse_response: bool = True, - memory: Optional[Union[TeamMemory, Memory]] = None, - enable_agentic_memory: bool = False, - enable_user_memories: bool = False, - add_memory_references: Optional[bool] = None, - enable_session_summaries: bool = False, - add_session_summary_references: Optional[bool] = None, - enable_team_history: bool = False, - add_history_to_messages: bool = False, - num_of_interactions_from_history: Optional[int] = None, - num_history_runs: int = 3, - storage: Optional[Storage] = None, - extra_data: Optional[Dict[str, Any]] = None, - reasoning: bool = False, - reasoning_model: Optional[Model] = None, - reasoning_agent: Optional[Agent] = None, - reasoning_min_steps: int = 1, - reasoning_max_steps: int = 10, - stream: Optional[bool] = None, - stream_intermediate_steps: bool = False, - debug_mode: bool = False, - show_members_responses: bool = False, - monitoring: bool = False, - telemetry: bool = True, - ): - self.members = members - - self.mode = mode - - self.model = model - - self.name = name - self.team_id = team_id - - self.user_id = user_id - self.session_id = session_id - self.session_name = session_name - self.session_state = session_state - self.team_session_state = team_session_state - self.add_state_in_messages = add_state_in_messages - - self.description = description - self.instructions = instructions - self.expected_output = expected_output - self.additional_context = additional_context - self.markdown = markdown - self.add_datetime_to_instructions = add_datetime_to_instructions - self.add_location_to_instructions = add_location_to_instructions - self.add_member_tools_to_system_message = add_member_tools_to_system_message - self.system_message = system_message - self.system_message_role = system_message_role - self.success_criteria = success_criteria - - self.context = context - self.add_context = add_context - - self.knowledge = knowledge - self.knowledge_filters = knowledge_filters - self.enable_agentic_knowledge_filters = enable_agentic_knowledge_filters - self.add_references = add_references - self.retriever = retriever - self.references_format = references_format - - self.enable_agentic_context = enable_agentic_context - self.share_member_interactions = share_member_interactions - self.get_member_information_tool = get_member_information_tool - self.search_knowledge = search_knowledge - self.read_team_history = read_team_history - - self.tools = tools - self.show_tool_calls = show_tool_calls - self.tool_choice = tool_choice - self.tool_call_limit = tool_call_limit - self.tool_hooks = tool_hooks - - self.response_model = response_model - self.use_json_mode = use_json_mode - self.parse_response = parse_response - - self.memory = memory - self.enable_agentic_memory = enable_agentic_memory - self.enable_user_memories = enable_user_memories - self.add_memory_references = add_memory_references - self.enable_session_summaries = enable_session_summaries - self.add_session_summary_references = add_session_summary_references - - self.enable_team_history = enable_team_history - self.add_history_to_messages = add_history_to_messages - self.num_of_interactions_from_history = num_of_interactions_from_history - self.num_history_runs = num_history_runs - - self.storage = storage - self.extra_data = extra_data - - self.reasoning = reasoning - self.reasoning_model = reasoning_model - self.reasoning_agent = reasoning_agent - self.reasoning_min_steps = reasoning_min_steps - self.reasoning_max_steps = reasoning_max_steps - - self.stream = stream - self.stream_intermediate_steps = stream_intermediate_steps - - self.debug_mode = debug_mode - self.show_members_responses = show_members_responses - - self.monitoring = monitoring - self.telemetry = telemetry - - # --- Params not to be set by user --- - self.session_metrics: Optional[SessionMetrics] = None - self.full_team_session_metrics: Optional[SessionMetrics] = None - - self.run_id: Optional[str] = None - self.run_input: Optional[Union[str, List, Dict]] = None - self.run_messages: Optional[RunMessages] = None - self.run_response: Optional[TeamRunResponse] = None - - # Images generated during this session - self.images: Optional[List[ImageArtifact]] = None - # Audio generated during this session - self.audio: Optional[List[AudioArtifact]] = None - # Videos generated during this session - self.videos: Optional[List[VideoArtifact]] = None - - # Team session - self.team_session: Optional[TeamSession] = None - - self._tool_instructions: Optional[List[str]] = None - self._functions_for_model: Optional[Dict[str, Function]] = None - self._tools_for_model: Optional[List[Dict[str, Any]]] = None - - # True if we should parse a member response model - self._member_response_model: Optional[Type[BaseModel]] = None - - self._formatter: Optional[SafeFormatter] = None - - def _set_team_id(self) -> str: - if self.team_id is None: - self.team_id = str(uuid4()) - return self.team_id - - def _set_debug(self) -> None: - if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true": - self.debug_mode = True - set_log_level_to_debug(source_type="team") - else: - set_log_level_to_info(source_type="team") - - def _set_storage_mode(self) -> None: - if self.storage is not None: - self.storage.mode = "team" - - def _set_monitoring(self) -> None: - """Override monitoring and telemetry settings based on environment variables.""" - - # Only override if the environment variable is set - monitor_env = getenv("AGNO_MONITOR") - if monitor_env is not None: - self.monitoring = monitor_env.lower() == "true" - - telemetry_env = getenv("AGNO_TELEMETRY") - if telemetry_env is not None: - self.telemetry = telemetry_env.lower() == "true" - - def _initialize_member( - self, member: Union["Team", Agent], session_id: Optional[str] = None - ) -> None: - # Set debug mode for all members - if self.debug_mode: - member.debug_mode = True - if self.show_tool_calls: - member.show_tool_calls = True - if self.markdown: - member.markdown = True - - if session_id is not None: - member.team_session_id = session_id - - # Set the team session state on members - if self.team_session_state is not None: - if member.team_session_state is None: - member.team_session_state = self.team_session_state - else: - merge_dictionaries(member.team_session_state, self.team_session_state) - - if isinstance(member, Agent): - member.team_id = self.team_id - member.set_agent_id() - elif isinstance(member, Team): - if member.team_id is None: - member.team_id = str(uuid4()) - member.parent_team_id = self.team_id - for sub_member in member.members: - self._initialize_member(sub_member, session_id) - if member.name is None: - log_warning("Team member name is undefined.") - - def _set_default_model(self) -> None: - # Set the default model - if self.model is None: - try: - from agno.models.openai import OpenAIChat - except ModuleNotFoundError as e: - log_exception(e) - log_error( - "Agno agents use `openai` as the default model provider. " - "Please provide a `model` or install `openai`." - ) - exit(1) - - log_info("Setting default model to OpenAI Chat") - self.model = OpenAIChat(id="gpt-4o") - - def _set_defaults(self) -> None: - if self.add_memory_references is None: - self.add_memory_references = ( - self.enable_user_memories or self.enable_agentic_memory - ) - - if self.add_session_summary_references is None: - self.add_session_summary_references = self.enable_session_summaries - - if self.num_of_interactions_from_history is not None: - self.num_history_runs = self.num_of_interactions_from_history - - def _reset_session_state(self) -> None: - self.session_name = None - self.session_state = None - self.team_session_state = None - self.session_metrics = None - self.images = None - self.videos = None - self.audio = None - self.files = None - self.team_session = None - - def _reset_run_state(self) -> None: - self.run_id = None - self.run_input = None - self.run_messages = None - self.run_response = None - - def initialize_team(self, session_id: Optional[str] = None) -> None: - self._set_defaults() - self._set_default_model() - self._set_storage_mode() - - # Set debug mode - self._set_debug() - - # Set monitoring and telemetry - self._set_monitoring() - - # Set the team ID if not set - self._set_team_id() - - log_debug(f"Team ID: {self.team_id}", center=True) - - # Initialize memory if not yet set - if self.memory is None: - self.memory = Memory() - - # Default to the team's model if no model is provided - if isinstance(self.memory, Memory): - if self.memory.model is None and self.model is not None: - self.memory.set_model(self.model) - - # Initialize formatter - if self._formatter is None: - self._formatter = SafeFormatter() - - for member in self.members: - self._initialize_member(member, session_id=session_id) - - # Make sure for the team, we are using the team logger - use_team_logger() - - @property - def is_streamable(self) -> bool: - return self.response_model is None - - @overload - def run( - self, - message: Union[str, List, Dict, Message], - *, - stream: Literal[False] = False, - stream_intermediate_steps: Optional[bool] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - retries: Optional[int] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> TeamRunResponse: ... - - @overload - def run( - self, - message: Union[str, List, Dict, Message], - *, - stream: Literal[True] = True, - stream_intermediate_steps: Optional[bool] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - retries: Optional[int] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> Iterator[Union[RunResponseEvent, TeamRunResponseEvent]]: ... - - def run( - self, - message: Union[str, List, Dict, Message], - *, - stream: Optional[bool] = None, - stream_intermediate_steps: Optional[bool] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - retries: Optional[int] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> Union[ - TeamRunResponse, Iterator[Union[RunResponseEvent, TeamRunResponseEvent]] - ]: - """Run the Team and return the response.""" - - self._reset_run_state() - - if session_id is not None: - # Reset session state if a session_id is provided. Session name and session state will be loaded from storage. - self._reset_session_state() - - # Use the default user_id and session_id when necessary - if user_id is None: - user_id = self.user_id - - if session_id is None or session_id == "": - # Default to the team's session_id if no session_id is provided - if not (self.session_id is None or self.session_id == ""): - session_id = self.session_id - else: - # Generate a new session_id and store it in the agent - session_id = str(uuid4()) - self.session_id = session_id - - session_id = cast(str, session_id) - - self._initialize_session_state(user_id=user_id, session_id=session_id) - - log_debug(f"Session ID: {session_id}", center=True) - - # Initialize Team - self.initialize_team(session_id=session_id) - - # Initialize Knowledge Filters - effective_filters = knowledge_filters - - # When filters are passed manually - if self.knowledge_filters or knowledge_filters: - """ - initialize metadata (specially required in case when load is commented out) - when load is not called the reader's document_lists won't be called and metadata filters won't be initialized - so we need to call initialize_valid_filters to make sure the filters are initialized - """ - if not self.knowledge.valid_metadata_filters: # type: ignore - self.knowledge.initialize_valid_filters() # type: ignore - - effective_filters = self._get_team_effective_filters(knowledge_filters) - - # Agentic filters are enabled - if ( - self.enable_agentic_knowledge_filters - and not self.knowledge.valid_metadata_filters - ): # type: ignore - # initialize metadata (specially required in case when load is commented out) - self.knowledge.initialize_valid_filters() # type: ignore - - # Use stream override value when necessary - if stream is None: - stream = False if self.stream is None else self.stream - - if stream_intermediate_steps is None: - stream_intermediate_steps = ( - False - if self.stream_intermediate_steps is None - else self.stream_intermediate_steps - ) - - # Can't have stream_intermediate_steps if stream is False - if stream is False: - stream_intermediate_steps = False - - self.stream = self.stream or (stream and self.is_streamable) - self.stream_intermediate_steps = self.stream_intermediate_steps or ( - stream_intermediate_steps and self.stream - ) - - # Read existing session from storage - self.read_from_storage(session_id=session_id) - - # Read existing session from storage - if self.context is not None: - self._resolve_run_context() - - if self.response_model is not None and self.parse_response and stream is True: - # Disable stream if response_model is set - stream = False - log_debug("Disabling stream as response_model is set") - - # Configure the model for runs - self._set_default_model() - response_format: Optional[Union[Dict, Type[BaseModel]]] = ( - self._get_response_format() - ) - - self.model = cast(Model, self.model) - self.determine_tools_for_model( - model=self.model, - session_id=session_id, - user_id=user_id, - async_mode=False, - knowledge_filters=effective_filters, - message=message, - images=images, - videos=videos, - audio=audio, - files=files, - ) - - # Register the team on the platform - thread = threading.Thread(target=self.register_team) - thread.start() - - # Create a run_id for this specific run - run_id = str(uuid4()) - - # Create a new run_response for this attempt - run_response = TeamRunResponse( - run_id=run_id, session_id=session_id, team_id=self.team_id - ) - - run_response.model = self.model.id if self.model is not None else None - run_response.model_provider = ( - self.model.provider if self.model is not None else None - ) - - self.run_response = run_response - self.run_id = run_id - - retries = retries or 3 - - # Run the team - last_exception = None - num_attempts = retries + 1 - - for attempt in range(num_attempts): - # Initialize the current run - - log_debug(f"Team Run Start: {self.run_id}", center=True) - log_debug(f"Mode: '{self.mode}'", center=True) - - # Set run_input - if message is not None: - if isinstance(message, str): - self.run_input = message - elif isinstance(message, Message): - self.run_input = message.to_dict() - else: - self.run_input = message - - # Run the team - try: - # Prepare run messages - if self.mode == "route": - run_messages: RunMessages = self.get_run_messages( - session_id=session_id, - user_id=user_id, - message=message, - audio=audio, - images=images, - videos=videos, - files=files, - knowledge_filters=effective_filters, - **kwargs, - ) - else: - run_messages = self.get_run_messages( - session_id=session_id, - user_id=user_id, - message=message, - audio=audio, - images=images, - videos=videos, - files=files, - knowledge_filters=effective_filters, - **kwargs, - ) - self.run_messages = run_messages - if len(run_messages.messages) == 0: - log_error("No messages to be sent to the model.") - - if stream and self.is_streamable: - response_iterator = self._run_stream( - run_response=self.run_response, - run_messages=run_messages, - stream_intermediate_steps=stream_intermediate_steps, - session_id=session_id, - user_id=user_id, - response_format=response_format, - ) - - return response_iterator - else: - return self._run( - run_response=self.run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - response_format=response_format, - ) - - except ModelProviderError as e: - import time - - log_warning(f"Attempt {attempt + 1}/{num_attempts} failed: {str(e)}") - - last_exception = e - if attempt < num_attempts - 1: - time.sleep(2**attempt) - except (KeyboardInterrupt, RunCancelledException): - if stream and self.is_streamable: - return self._generator_wrapper( - create_team_run_response_cancelled_event( - run_response, "Operation cancelled by user" - ) - ) - else: - return self._create_run_response( - run_state=RunStatus.cancelled, - content="Operation cancelled by user", - from_run_response=run_response, - session_id=session_id, - ) - - # If we get here, all retries failed - if last_exception is not None: - log_error( - f"Failed after {num_attempts} attempts. Last error using {last_exception.model_name}({last_exception.model_id})" - ) - if stream and self.is_streamable: - return self._generator_wrapper( - create_team_run_response_error_event( - run_response, error=str(last_exception) - ) - ) - - raise last_exception - else: - if stream and self.is_streamable: - return self._generator_wrapper( - create_team_run_response_error_event( - run_response, error=str(last_exception) - ) - ) - - raise Exception(f"Failed after {num_attempts} attempts.") - - def _run( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - user_id: Optional[str] = None, - response_format: Optional[Union[Dict, Type[BaseModel]]] = None, - ) -> TeamRunResponse: - """Run the Team and return the response. - - Steps: - 1. Reason about the task(s) if reasoning is enabled - 2. Get a response from the model - 3. Update Team Memory - 5. Save session to storage - 6. Parse any structured outputs - 7. Log the team run - """ - # 1. Reason about the task(s) if reasoning is enabled - self._handle_reasoning(run_response=run_response, run_messages=run_messages) - - # Update agent state - index_of_last_user_message = len(run_messages.messages) - - # 2. Get the model response for the team leader - self.model = cast(Model, self.model) - model_response: ModelResponse = self.model.response( - messages=run_messages.messages, - response_format=response_format, - tools=self._tools_for_model, - functions=self._functions_for_model, - tool_choice=self.tool_choice, - tool_call_limit=self.tool_call_limit, - ) - - # Update TeamRunResponse - self._update_run_response( - model_response=model_response, - run_response=run_response, - run_messages=run_messages, - ) - - # 3. Add the run to memory - self._add_run_to_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - index_of_last_user_message=index_of_last_user_message, - ) - - # 4. Update Team Memory - response_iterator = self._update_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - ) - deque(response_iterator, maxlen=0) - - # 5. Save session to storage - self.write_to_storage(session_id=session_id, user_id=user_id) - - # 6. Parse team response model - self._convert_response_to_structured_format(run_response=run_response) - - # 8. Log Team Run - self._log_team_run(session_id=session_id, user_id=user_id) - - log_debug(f"Team Run End: {self.run_id}", center=True, symbol="*") - - return run_response - - def _run_stream( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - user_id: Optional[str] = None, - response_format: Optional[Union[Dict, Type[BaseModel]]] = None, - stream_intermediate_steps: bool = False, - ) -> Iterator[Union[TeamRunResponseEvent, RunResponseEvent]]: - """Run the Team and return the response iterator. - - Steps: - 1. Reason about the task(s) if reasoning is enabled - 2. Get a response from the model - 3. Update Team Memory - 4. Save session to storage - 5. Log Team Run - """ - - # 1. Reason about the task(s) if reasoning is enabled - yield from self._handle_reasoning_stream( - run_response=run_response, - run_messages=run_messages, - ) - - # Update agent state - index_of_last_user_message = len(run_messages.messages) - - # Start the Run by yielding a RunStarted event - if stream_intermediate_steps: - yield create_team_run_response_started_event(run_response) - - # 2. Get a response from the model - yield from self._handle_model_response_stream( - run_response=run_response, - run_messages=run_messages, - response_format=response_format, - stream_intermediate_steps=stream_intermediate_steps, - ) - - # 3. Add the run to memory - self._add_run_to_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - index_of_last_user_message=index_of_last_user_message, - ) - - # 4. Update Team Memory - yield from self._update_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - ) - - # 4. Save session to storage - self.write_to_storage(session_id=session_id, user_id=user_id) - - # 5. Log Team Run - self._log_team_run(session_id=session_id, user_id=user_id) - - if stream_intermediate_steps: - yield create_team_run_response_completed_event( - from_run_response=run_response, - ) - - log_debug(f"Team Run End: {self.run_id}", center=True, symbol="*") - - @overload - async def arun( - self, - message: Union[str, List, Dict, Message], - *, - stream: Literal[False] = False, - stream_intermediate_steps: Optional[bool] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - retries: Optional[int] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> TeamRunResponse: ... - - @overload - async def arun( - self, - message: Union[str, List, Dict, Message], - *, - stream: Literal[True] = True, - stream_intermediate_steps: Optional[bool] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - retries: Optional[int] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> AsyncIterator[Union[RunResponseEvent, TeamRunResponseEvent]]: ... - - async def arun( - self, - message: Union[str, List, Dict, Message], - *, - stream: Optional[bool] = None, - stream_intermediate_steps: Optional[bool] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - retries: Optional[int] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> Union[ - TeamRunResponse, AsyncIterator[Union[RunResponseEvent, TeamRunResponseEvent]] - ]: - """Run the Team asynchronously and return the response.""" - - self._reset_run_state() - - if session_id is not None: - # Reset session state if a session_id is provided. Session name and session state will be loaded from storage. - self._reset_session_state() - - # Use the default user_id and session_id when necessary - if user_id is None: - user_id = self.user_id - - if session_id is None or session_id == "": - # Default to the team's session_id if no session_id is provided - if not (self.session_id is None or self.session_id == ""): - session_id = self.session_id - else: - # Generate a new session_id and store it in the team - session_id = str(uuid4()) - self.session_id = session_id - - session_id = cast(str, session_id) - - self._initialize_session_state(user_id=user_id, session_id=session_id) - - log_debug(f"Session ID: {session_id}", center=True) - - self.initialize_team(session_id=session_id) - - effective_filters = knowledge_filters - - # When filters are passed manually - if self.knowledge_filters or knowledge_filters: - """ - initialize metadata (specially required in case when load is commented out) - when load is not called the reader's document_lists won't be called and metadata filters won't be initialized - so we need to call initialize_valid_filters to make sure the filters are initialized - """ - if not self.knowledge.valid_metadata_filters: # type: ignore - self.knowledge.initialize_valid_filters() # type: ignore - - effective_filters = self._get_team_effective_filters(knowledge_filters) - - # Use stream override value when necessary - if stream is None: - stream = False if self.stream is None else self.stream - - if stream_intermediate_steps is None: - stream_intermediate_steps = ( - False - if self.stream_intermediate_steps is None - else self.stream_intermediate_steps - ) - - # Can't have stream_intermediate_steps if stream is False - if stream is False: - stream_intermediate_steps = False - - self.stream = self.stream or (stream and self.is_streamable) - self.stream_intermediate_steps = self.stream_intermediate_steps or ( - stream_intermediate_steps and self.stream - ) - - # Read existing session from storage - self.read_from_storage(session_id=session_id) - - # Read existing session from storage - if self.context is not None: - self._resolve_run_context() - - if self.response_model is not None and self.parse_response and stream is True: - # Disable stream if response_model is set - stream = False - log_debug("Disabling stream as response_model is set") - - # Configure the model for runs - self._set_default_model() - response_format = self._get_response_format() - - self.model = cast(Model, self.model) - self.determine_tools_for_model( - model=self.model, - session_id=session_id, - user_id=user_id, - async_mode=True, - knowledge_filters=effective_filters, - message=message, - images=images, - videos=videos, - audio=audio, - files=files, - ) - - asyncio.create_task(self._aregister_team()) - - # Create a run_id for this specific run - run_id = str(uuid4()) - - # Create a new run_response for this attempt - run_response = TeamRunResponse( - run_id=run_id, session_id=session_id, team_id=self.team_id - ) - - run_response.model = self.model.id if self.model is not None else None - run_response.model_provider = ( - self.model.provider if self.model is not None else None - ) - - self.run_response = run_response - self.run_id = run_id - - retries = retries or 3 - - # Run the team - last_exception = None - num_attempts = retries + 1 - - for attempt in range(num_attempts): - log_debug(f"Team Run Start: {self.run_id}", center=True) - log_debug(f"Mode: '{self.mode}'", center=True) - - # Set run_input - if message is not None: - if isinstance(message, str): - self.run_input = message - elif isinstance(message, Message): - self.run_input = message.to_dict() - else: - self.run_input = message - - # Run the team - try: - # Prepare run messages - if self.mode == "route": - # In route mode the model shouldn't get images/audio/video - run_messages: RunMessages = self.get_run_messages( - session_id=session_id, - user_id=user_id, - message=message, - audio=audio, - images=images, - videos=videos, - files=files, - knowledge_filters=effective_filters, - **kwargs, - ) - else: - run_messages = self.get_run_messages( - session_id=session_id, - user_id=user_id, - message=message, - audio=audio, - images=images, - videos=videos, - files=files, - knowledge_filters=effective_filters, - **kwargs, - ) - - if stream: - response_iterator = self._arun_stream( - run_response=self.run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - response_format=response_format, - stream_intermediate_steps=stream_intermediate_steps, - ) - return response_iterator - else: - return await self._arun( - run_response=self.run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - response_format=response_format, - ) - - except ModelProviderError as e: - log_warning(f"Attempt {attempt + 1}/{num_attempts} failed: {str(e)}") - last_exception = e - if attempt < num_attempts - 1: - await asyncio.sleep(2**attempt) - except (KeyboardInterrupt, RunCancelledException): - if stream and self.is_streamable: - return self._async_generator_wrapper( - create_team_run_response_cancelled_event( - run_response, "Operation cancelled by user" - ) - ) - else: - return self._create_run_response( - run_state=RunStatus.cancelled, - content="Operation cancelled by user", - from_run_response=run_response, - session_id=session_id, - ) - - # If we get here, all retries failed - if last_exception is not None: - log_error( - f"Failed after {num_attempts} attempts. Last error using {last_exception.model_name}({last_exception.model_id})" - ) - if stream and self.is_streamable: - return self._async_generator_wrapper( - create_team_run_response_error_event( - run_response, error=str(last_exception) - ) - ) - - raise last_exception - else: - if stream and self.is_streamable: - return self._async_generator_wrapper( - create_team_run_response_error_event( - run_response, error=str(last_exception) - ) - ) - - raise Exception(f"Failed after {num_attempts} attempts.") - - async def _arun( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - user_id: Optional[str] = None, - response_format: Optional[Union[Dict, Type[BaseModel]]] = None, - ) -> TeamRunResponse: - """Run the Team and return the response. - - Steps: - 1. Reason about the task(s) if reasoning is enabled - 2. Get a response from the model - 3. Update Team Memory - 5. Save session to storage - 6. Parse any structured outputs - 7. Log the team run - """ - - self.model = cast(Model, self.model) - - # 1. Reason about the task(s) if reasoning is enabled - await self._ahandle_reasoning( - run_response=run_response, run_messages=run_messages - ) - - # Update agent state - index_of_last_user_message = len(run_messages.messages) - - # 2. Get the model response for the team leader - model_response = await self.model.aresponse( - messages=run_messages.messages, - response_format=response_format, - tools=self._tools_for_model, - functions=self._functions_for_model, - tool_choice=self.tool_choice, - tool_call_limit=self.tool_call_limit, - ) # type: ignore - - # Update TeamRunResponse - self._update_run_response( - model_response=model_response, - run_response=run_response, - run_messages=run_messages, - ) - - # 3. Add the run to memory - self._add_run_to_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - index_of_last_user_message=index_of_last_user_message, - ) - # 4. Update Team Memory - async for _ in self._aupdate_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - ): - pass - - # 5. Save session to storage - self.write_to_storage(session_id=session_id, user_id=user_id) - - # 6. Parse team response model - self._convert_response_to_structured_format(run_response=run_response) - - # 7. Log Team Run - await self._alog_team_run(session_id=session_id, user_id=user_id) - - log_debug(f"Team Run End: {self.run_id}", center=True, symbol="*") - - return run_response - - async def _arun_stream( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - user_id: Optional[str] = None, - response_format: Optional[Union[Dict, Type[BaseModel]]] = None, - stream_intermediate_steps: bool = False, - ) -> AsyncIterator[Union[TeamRunResponseEvent, RunResponseEvent]]: - """Run the Team and return the response. - - Steps: - 1. Reason about the task(s) if reasoning is enabled - 2. Get a response from the model - 3. Update Team Memory - 4. Save session to storage - 5. Log Team Run - """ - - # 1. Reason about the task(s) if reasoning is enabled - async for item in self._ahandle_reasoning_stream( - run_response=run_response, run_messages=run_messages - ): - yield item - - # Update agent state - index_of_last_user_message = len(run_messages.messages) - - # Start the Run by yielding a RunStarted event - if stream_intermediate_steps: - yield create_team_run_response_started_event(from_run_response=run_response) - - # 2. Get a response from the model - async for event in self._ahandle_model_response_stream( - run_response=run_response, - run_messages=run_messages, - response_format=response_format, - stream_intermediate_steps=stream_intermediate_steps, - ): - yield event - - # 3. Add the run to memory - self._add_run_to_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - index_of_last_user_message=index_of_last_user_message, - ) - - # 4. Update Team Memory - async for event in self._aupdate_memory( - run_response=run_response, - run_messages=run_messages, - session_id=session_id, - user_id=user_id, - ): - yield event - - # 5. Save session to storage - self.write_to_storage(session_id=session_id, user_id=user_id) - - # 6. Log Team Run - await self._alog_team_run(session_id=session_id, user_id=user_id) - - if stream_intermediate_steps: - yield create_team_run_response_completed_event( - from_run_response=run_response - ) - - log_debug(f"Team Run End: {self.run_id}", center=True, symbol="*") - - def _update_run_response( - self, - model_response: ModelResponse, - run_response: TeamRunResponse, - run_messages: RunMessages, - ): - # Handle structured outputs - if ( - (self.response_model is not None) - and not self.use_json_mode - and (model_response.parsed is not None) - ): - # Update the run_response content with the structured output - run_response.content = model_response.parsed - # Update the run_response content_type with the structured output class name - run_response.content_type = self.response_model.__name__ - else: - # Update the run_response content with the model response content - if not run_response.content: - run_response.content = model_response.content - else: - run_response.content += model_response.content - - # Update the run_response thinking with the model response thinking - if model_response.thinking is not None: - if not run_response.thinking: - run_response.thinking = model_response.thinking - else: - run_response.thinking += model_response.thinking - - # Update citations - if model_response.citations is not None: - run_response.citations = model_response.citations - - # Update the run_response tools with the model response tool_executions - if model_response.tool_executions is not None: - if run_response.tools is None: - run_response.tools = model_response.tool_executions - else: - run_response.tools.extend(model_response.tool_executions) - - run_response.formatted_tool_calls = format_tool_calls(run_response.tools or []) - - # Update the run_response audio with the model response audio - if model_response.audio is not None: - run_response.response_audio = model_response.audio - - # Update the run_response created_at with the model response created_at - run_response.created_at = model_response.created_at - - # Build a list of messages that should be added to the RunResponse - messages_for_run_response = [ - m for m in run_messages.messages if m.add_to_agent_memory - ] - - # Update the TeamRunResponse messages - run_response.messages = messages_for_run_response - - # Update the TeamRunResponse metrics - run_response.metrics = self._aggregate_metrics_from_messages( - messages_for_run_response - ) - - for tool_call in model_response.tool_calls: - tool_name = tool_call.get("tool_name", "") - if tool_name.lower() in ["think", "analyze"]: - tool_args = tool_call.get("tool_args", {}) - self.update_reasoning_content_from_tool_call( - run_response, tool_name, tool_args - ) - - def _add_run_to_memory( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - index_of_last_user_message: int = 0, - ): - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - else: - self.memory = cast(Memory, self.memory) - - if isinstance(self.memory, TeamMemory): - # Add the system message to the memory - if run_messages.system_message is not None: - self.memory.add_system_message( - run_messages.system_message, - system_message_role=self.system_message_role, - ) - - # Build a list of messages that should be added to the AgentMemory - messages_for_memory: List[Message] = ( - [run_messages.user_message] - if run_messages.user_message is not None - else [] - ) - # Add messages from messages_for_run after the last user message - for _rm in run_messages.messages[index_of_last_user_message:]: - if _rm.add_to_agent_memory: - messages_for_memory.append(_rm) - if len(messages_for_memory) > 0: - self.memory.add_messages(messages=messages_for_memory) - - team_run = TeamRun(response=run_response) - team_run.message = run_messages.user_message - - # Update the memories with the user message if needed - if ( - self.memory is not None - and self.memory.create_user_memories - and self.memory.update_user_memories_after_run - and run_messages.user_message is not None - ): - self.memory.update_memory( - input=run_messages.user_message.get_content_string() - ) # type: ignore - - # Add AgentRun to memory - self.memory.add_team_run(team_run) # type: ignore - - elif isinstance(self.memory, Memory): - # Add AgentRun to memory - self.memory.add_run(session_id=session_id, run=run_response) - - def _update_memory( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - user_id: Optional[str] = None, - ) -> Iterator[TeamRunResponseEvent]: - if isinstance(self.memory, TeamMemory): - # Update the memories with the user message if needed - if ( - self.memory is not None - and self.memory.create_user_memories - and self.memory.update_user_memories_after_run - and run_messages.user_message is not None - ): - if self.stream_intermediate_steps: - yield create_team_memory_update_started_event( - from_run_response=run_response - ) - self.memory.update_memory( - input=run_messages.user_message.get_content_string() - ) # type: ignore - - if self.stream_intermediate_steps: - yield create_team_memory_update_completed_event( - from_run_response=run_response - ) - - # Add AgentRun to memory - self.session_metrics = self._calculate_session_metrics(self.memory.messages) - self.full_team_session_metrics = self._calculate_full_team_session_metrics( - self.memory.messages - ) - elif isinstance(self.memory, Memory): - yield from self._make_memories_and_summaries( - run_messages, session_id, user_id - ) - - session_messages: List[Message] = [] - for run in self.memory.runs.get(session_id, []): # type: ignore - if run.messages is not None: - for m in run.messages: - session_messages.append(m) - - # 10. Calculate session metrics - self.session_metrics = self._calculate_session_metrics(session_messages) - - async def _aupdate_memory( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - session_id: str, - user_id: Optional[str] = None, - ): - if isinstance(self.memory, TeamMemory): - # Update the memories with the user message if needed - if ( - self.memory is not None - and self.memory.create_user_memories - and self.memory.update_user_memories_after_run - and run_messages.user_message is not None - ): - if self.stream_intermediate_steps: - yield create_team_memory_update_started_event( - from_run_response=run_response - ) - - await self.memory.aupdate_memory( - input=run_messages.user_message.get_content_string() - ) - - if self.stream_intermediate_steps: - yield create_team_memory_update_completed_event( - from_run_response=run_response - ) - - # Calculate session metrics - self.session_metrics = self._calculate_session_metrics(self.memory.messages) - self.full_team_session_metrics = self._calculate_full_team_session_metrics( - self.memory.messages - ) - - elif isinstance(self.memory, Memory): - async for event in self._amake_memories_and_summaries( - run_messages, session_id, user_id - ): - yield event - - session_messages: List[Message] = [] - if self.memory.runs: - for run in self.memory.runs.get(session_id, []): - if run.messages is not None: - for m in run.messages: - session_messages.append(m) - - # 10. Calculate session metrics - self.session_metrics = self._calculate_session_metrics(session_messages) - - def _handle_model_response_stream( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - response_format: Optional[Union[Dict, Type[BaseModel]]] = None, - stream_intermediate_steps: bool = False, - ) -> Iterator[Union[TeamRunResponseEvent, RunResponseEvent]]: - self.model = cast(Model, self.model) - - reasoning_state = { - "reasoning_started": False, - "reasoning_time_taken": 0.0, - } - - full_model_response = ModelResponse() - for model_response_event in self.model.response_stream( - messages=run_messages.messages, - response_format=response_format, - tools=self._tools_for_model, - functions=self._functions_for_model, - tool_choice=self.tool_choice, - tool_call_limit=self.tool_call_limit, - ): - yield from self._handle_model_response_chunk( - run_response=run_response, - full_model_response=full_model_response, - model_response_event=model_response_event, - stream_intermediate_steps=stream_intermediate_steps, - reasoning_state=reasoning_state, - ) - - # 3. Update TeamRunResponse - run_response.created_at = full_model_response.created_at - if full_model_response.content is not None: - run_response.content = full_model_response.content - if full_model_response.thinking is not None: - run_response.thinking = full_model_response.thinking - if full_model_response.audio is not None: - run_response.response_audio = full_model_response.audio - if full_model_response.citations is not None: - run_response.citations = full_model_response.citations - - if stream_intermediate_steps and reasoning_state["reasoning_started"]: - all_reasoning_steps: List[ReasoningStep] = [] - if ( - self.run_response - and self.run_response.extra_data - and hasattr(self.run_response.extra_data, "reasoning_steps") - ): - all_reasoning_steps = cast( - List[ReasoningStep], self.run_response.extra_data.reasoning_steps - ) - - if all_reasoning_steps: - self._add_reasoning_metrics_to_extra_data( - run_response, reasoning_state["reasoning_time_taken"] - ) - yield create_team_reasoning_completed_event( - from_run_response=run_response, - content=ReasoningSteps(reasoning_steps=all_reasoning_steps), - content_type=ReasoningSteps.__name__, - ) - - # Build a list of messages that should be added to the RunResponse - messages_for_run_response = [ - m for m in run_messages.messages if m.add_to_agent_memory - ] - # Update the TeamRunResponse messages - run_response.messages = messages_for_run_response - # Update the TeamRunResponse metrics - run_response.metrics = self._aggregate_metrics_from_messages( - messages_for_run_response - ) - - # Update the run_response audio if streaming - if full_model_response.audio is not None: - run_response.response_audio = full_model_response.audio - - async def _ahandle_model_response_stream( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - response_format: Optional[Union[Dict, Type[BaseModel]]] = None, - stream_intermediate_steps: bool = False, - ) -> AsyncIterator[Union[TeamRunResponseEvent, RunResponseEvent]]: - self.model = cast(Model, self.model) - - reasoning_state = { - "reasoning_started": False, - "reasoning_time_taken": 0.0, - } - full_model_response = ModelResponse() - model_stream = self.model.aresponse_stream( - messages=run_messages.messages, - response_format=response_format, - tools=self._tools_for_model, - functions=self._functions_for_model, - tool_choice=self.tool_choice, - tool_call_limit=self.tool_call_limit, - ) # type: ignore - async for model_response_event in model_stream: - for chunk in self._handle_model_response_chunk( - run_response=run_response, - full_model_response=full_model_response, - model_response_event=model_response_event, - stream_intermediate_steps=stream_intermediate_steps, - reasoning_state=reasoning_state, - ): - yield chunk - - # Handle structured outputs - if ( - (self.response_model is not None) - and not self.use_json_mode - and (full_model_response.parsed is not None) - ): - # Update the run_response content with the structured output - run_response.content = full_model_response.parsed - - # Update TeamRunResponse - run_response.created_at = full_model_response.created_at - if full_model_response.content is not None: - run_response.content = full_model_response.content - if full_model_response.thinking is not None: - run_response.thinking = full_model_response.thinking - if full_model_response.audio is not None: - run_response.response_audio = full_model_response.audio - if full_model_response.citations is not None: - run_response.citations = full_model_response.citations - - # Build a list of messages that should be added to the RunResponse - messages_for_run_response = [ - m for m in run_messages.messages if m.add_to_agent_memory - ] - # Update the TeamRunResponse messages - run_response.messages = messages_for_run_response - # Update the TeamRunResponse metrics - run_response.metrics = self._aggregate_metrics_from_messages( - messages_for_run_response - ) - - if stream_intermediate_steps and reasoning_state["reasoning_started"]: - all_reasoning_steps: List[ReasoningStep] = [] - if ( - self.run_response - and self.run_response.extra_data - and hasattr(self.run_response.extra_data, "reasoning_steps") - ): - all_reasoning_steps = cast( - List[ReasoningStep], self.run_response.extra_data.reasoning_steps - ) - - if all_reasoning_steps: - self._add_reasoning_metrics_to_extra_data( - run_response, reasoning_state["reasoning_time_taken"] - ) - yield create_team_reasoning_completed_event( - from_run_response=run_response, - content=ReasoningSteps(reasoning_steps=all_reasoning_steps), - content_type=ReasoningSteps.__name__, - ) - - def _handle_model_response_chunk( - self, - run_response: TeamRunResponse, - full_model_response: ModelResponse, - model_response_event: Union[ - ModelResponse, TeamRunResponseEvent, RunResponseEvent - ], - reasoning_state: Dict[str, Any], - stream_intermediate_steps: bool = False, - ) -> Iterator[Union[TeamRunResponseEvent, RunResponseEvent]]: - if isinstance( - model_response_event, tuple(get_args(RunResponseEvent)) - ) or isinstance(model_response_event, tuple(get_args(TeamRunResponseEvent))): - # We just bubble the event up - yield model_response_event # type: ignore - else: - model_response_event = cast(ModelResponse, model_response_event) - # If the model response is an assistant_response, yield a RunResponse - if ( - model_response_event.event - == ModelResponseEvent.assistant_response.value - ): - should_yield = False - # Process content and thinking - if model_response_event.content is not None: - if not full_model_response.content: - full_model_response.content = model_response_event.content - else: - full_model_response.content += model_response_event.content - should_yield = True - - # Process thinking - if model_response_event.thinking is not None: - if not full_model_response.thinking: - full_model_response.thinking = model_response_event.thinking - else: - full_model_response.thinking += model_response_event.thinking - should_yield = True - - if model_response_event.citations is not None: - # We get citations in one chunk - full_model_response.citations = model_response_event.citations - should_yield = True - - # Process audio - if model_response_event.audio is not None: - if full_model_response.audio is None: - full_model_response.audio = AudioResponse( - id=str(uuid4()), content="", transcript="" - ) - - if model_response_event.audio.id is not None: - full_model_response.audio.id = model_response_event.audio.id # type: ignore - if model_response_event.audio.content is not None: - full_model_response.audio.content += ( - model_response_event.audio.content - ) # type: ignore - if model_response_event.audio.transcript is not None: - full_model_response.audio.transcript += ( - model_response_event.audio.transcript - ) # type: ignore - if model_response_event.audio.expires_at is not None: - full_model_response.audio.expires_at = ( - model_response_event.audio.expires_at - ) # type: ignore - if model_response_event.audio.mime_type is not None: - full_model_response.audio.mime_type = ( - model_response_event.audio.mime_type - ) # type: ignore - if model_response_event.audio.sample_rate is not None: - full_model_response.audio.sample_rate = ( - model_response_event.audio.sample_rate - ) - if model_response_event.audio.channels is not None: - full_model_response.audio.channels = ( - model_response_event.audio.channels - ) - - # Yield the audio and transcript bit by bit - should_yield = True - - if model_response_event.image is not None: - self.add_image(model_response_event.image) - - should_yield = True - - # Only yield the chunk - if should_yield: - yield create_team_run_response_content_event( - from_run_response=run_response, - content=model_response_event.content, - thinking=model_response_event.thinking, - redacted_thinking=model_response_event.redacted_thinking, - response_audio=full_model_response.audio, - citations=model_response_event.citations, - image=model_response_event.image, - ) - - # If the model response is a tool_call_started, add the tool call to the run_response - elif ( - model_response_event.event == ModelResponseEvent.tool_call_started.value - ): - # Add tool calls to the run_response - tool_executions_list = model_response_event.tool_executions - if tool_executions_list is not None: - # Add tool calls to the agent.run_response - if run_response.tools is None: - run_response.tools = tool_executions_list - else: - run_response.tools.extend(tool_executions_list) - - for tool in tool_executions_list: - yield create_team_tool_call_started_event( - from_run_response=run_response, - tool=tool, - ) - # Format tool calls whenever new ones are added during streaming - run_response.formatted_tool_calls = format_tool_calls( - run_response.tools or [] - ) - - # If the model response is a tool_call_completed, update the existing tool call in the run_response - elif ( - model_response_event.event - == ModelResponseEvent.tool_call_completed.value - ): - reasoning_step: Optional[ReasoningStep] = None - tool_executions_list = model_response_event.tool_executions - if tool_executions_list is not None: - # Update the existing tool call in the run_response - if run_response.tools: - # Create a mapping of tool_call_id to index - tool_call_index_map = { - tc.tool_call_id: i - for i, tc in enumerate(run_response.tools) - if tc.tool_call_id is not None - } - # Process tool calls - for tool_call_dict in tool_executions_list: - tool_call_id = tool_call_dict.tool_call_id or "" - index = tool_call_index_map.get(tool_call_id) - if index is not None: - run_response.tools[index] = tool_call_dict - else: - run_response.tools = tool_executions_list - - # Only iterate through new tool calls - for tool_call in tool_executions_list: - tool_name = tool_call.tool_name or "" - if tool_name.lower() in ["think", "analyze"]: - tool_args = tool_call.tool_args or {} - - reasoning_step = ( - self.update_reasoning_content_from_tool_call( - run_response, tool_name, tool_args - ) - ) - - metrics = tool_call.metrics - if metrics is not None and metrics.time is not None: - reasoning_state["reasoning_time_taken"] = ( - reasoning_state["reasoning_time_taken"] - + float(metrics.time) - ) - - yield create_team_tool_call_completed_event( - from_run_response=run_response, - tool=tool_call, - content=model_response_event.content, - ) - - if stream_intermediate_steps: - if reasoning_step is not None: - if not reasoning_state["reasoning_started"]: - yield create_team_reasoning_started_event( - from_run_response=run_response, - ) - reasoning_state["reasoning_started"] = True - - yield create_team_reasoning_step_event( - from_run_response=run_response, - reasoning_step=reasoning_step, - reasoning_content=run_response.reasoning_content or "", - ) - - def _convert_response_to_structured_format(self, run_response: TeamRunResponse): - # Convert the response to the structured format if needed - if self.response_model is not None and not isinstance( - run_response.content, self.response_model - ): - if isinstance(run_response.content, str) and self.parse_response: - try: - parsed_response_content = parse_response_model_str( - run_response.content, self.response_model - ) - - # Update TeamRunResponse - if parsed_response_content is not None: - run_response.content = parsed_response_content - run_response.content_type = self.response_model.__name__ - else: - log_warning("Failed to convert response to response_model") - except Exception as e: - log_warning(f"Failed to convert response to output model: {e}") - else: - log_warning( - "Something went wrong. Team run response content is not a string" - ) - elif self._member_response_model is not None and not isinstance( - run_response.content, self._member_response_model - ): - if isinstance(run_response.content, str): - try: - parsed_response_content = parse_response_model_str( - run_response.content, self._member_response_model - ) - # Update TeamRunResponse - if parsed_response_content is not None: - run_response.content = parsed_response_content - run_response.content_type = self._member_response_model.__name__ - else: - log_warning("Failed to convert response to response_model") - except Exception as e: - log_warning(f"Failed to convert response to output model: {e}") - else: - log_warning( - "Something went wrong. Member run response content is not a string" - ) - - def _initialize_session_state( - self, user_id: Optional[str] = None, session_id: Optional[str] = None - ) -> None: - self.session_state = self.session_state or {} - - if user_id is not None: - self.session_state["current_user_id"] = user_id - if self.team_session_state is not None: - self.team_session_state["current_user_id"] = user_id - - if session_id is not None: - self.session_state["current_session_id"] = session_id - if self.team_session_state is not None: - self.team_session_state["current_session_id"] = session_id - - def _make_memories_and_summaries( - self, run_messages: RunMessages, session_id: str, user_id: Optional[str] = None - ) -> Iterator[TeamRunResponseEvent]: - from concurrent.futures import ThreadPoolExecutor, as_completed - - self.run_response = cast(TeamRunResponse, self.run_response) - self.memory = cast(Memory, self.memory) - - # Create a thread pool with a reasonable number of workers - with ThreadPoolExecutor(max_workers=3) as executor: - futures = [] - user_message_str = ( - run_messages.user_message.get_content_string() - if run_messages.user_message is not None - else None - ) - if ( - self.enable_user_memories - and user_message_str is not None - and user_message_str - ): - futures.append( - executor.submit( - self.memory.create_user_memories, - message=user_message_str, - user_id=user_id, - ) - ) - - # Update the session summary if needed - if self.enable_session_summaries: - futures.append( - executor.submit( - self.memory.create_session_summary, - session_id=session_id, - user_id=user_id, - ) # type: ignore - ) - - if futures: - if self.stream_intermediate_steps: - yield create_team_memory_update_started_event( - from_run_response=self.run_response - ) - - # Wait for all operations to complete and handle any errors - for future in as_completed(futures): - try: - future.result() - except Exception as e: - log_warning(f"Error in memory/summary operation: {str(e)}") - - if self.stream_intermediate_steps: - yield create_team_memory_update_completed_event( - from_run_response=self.run_response - ) - - async def _amake_memories_and_summaries( - self, run_messages: RunMessages, session_id: str, user_id: Optional[str] = None - ) -> AsyncIterator[TeamRunResponseEvent]: - self.memory = cast(Memory, self.memory) - self.run_response = cast(TeamRunResponse, self.run_response) - tasks = [] - - user_message_str = ( - run_messages.user_message.get_content_string() - if run_messages.user_message is not None - else None - ) - if ( - self.enable_user_memories - and user_message_str is not None - and user_message_str - ): - tasks.append( - self.memory.acreate_user_memories( - message=user_message_str, user_id=user_id - ) - ) - - # Update the session summary if needed - if self.enable_session_summaries: - tasks.append( - self.memory.acreate_session_summary( - session_id=session_id, user_id=user_id - ) - ) # type: ignore - - if tasks: - if self.stream_intermediate_steps: - yield create_team_memory_update_started_event( - from_run_response=self.run_response - ) - - # Execute all tasks concurrently and handle any errors - try: - await asyncio.gather(*tasks) - except Exception as e: - log_warning(f"Error in memory/summary operation: {str(e)}") - - if self.stream_intermediate_steps: - yield create_team_memory_update_completed_event( - from_run_response=self.run_response - ) - - def _get_response_format(self) -> Optional[Union[Dict, Type[BaseModel]]]: - self.model = cast(Model, self.model) - if self.response_model is None: - return None - else: - json_response_format = {"type": "json_object"} - - if self.model.supports_native_structured_outputs: - if not self.use_json_mode: - log_debug("Setting Model.response_format to Agent.response_model") - return self.response_model - else: - log_debug( - "Model supports native structured outputs but it is not enabled. Using JSON mode instead." - ) - return json_response_format - - elif self.model.supports_json_schema_outputs: - if self.use_json_mode: - log_debug("Setting Model.response_format to JSON response mode") - return { - "type": "json_schema", - "json_schema": { - "name": self.response_model.__name__, - "schema": self.response_model.model_json_schema(), - }, - } - else: - return None - - else: - log_debug("Model does not support structured or JSON schema outputs.") - return json_response_format - - ########################################################################### - # Print Response - ########################################################################### - - def print_response( - self, - message: Optional[Union[List, Dict, str, Message]] = None, - *, - stream: bool = False, - stream_intermediate_steps: bool = False, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - show_message: bool = True, - show_reasoning: bool = True, - show_full_reasoning: bool = False, - console: Optional[Any] = None, - tags_to_include_in_markdown: Optional[Set[str]] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - markdown: Optional[bool] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> None: - if not tags_to_include_in_markdown: - tags_to_include_in_markdown = {"think", "thinking"} - - if markdown is None: - markdown = self.markdown - else: - self.markdown = markdown - - if self.response_model is not None: - stream = False - - if stream: - self._print_response_stream( - message=message, - console=console, - show_message=show_message, - show_reasoning=show_reasoning, - show_full_reasoning=show_full_reasoning, - tags_to_include_in_markdown=tags_to_include_in_markdown, - session_id=session_id, - user_id=user_id, - audio=audio, - images=images, - videos=videos, - files=files, - markdown=markdown, - stream_intermediate_steps=stream_intermediate_steps, - knowledge_filters=knowledge_filters, - **kwargs, - ) - else: - self._print_response( - message=message, - console=console, - show_message=show_message, - show_reasoning=show_reasoning, - show_full_reasoning=show_full_reasoning, - tags_to_include_in_markdown=tags_to_include_in_markdown, - session_id=session_id, - user_id=user_id, - audio=audio, - images=images, - videos=videos, - files=files, - markdown=markdown, - knowledge_filters=knowledge_filters, - **kwargs, - ) - - def _print_response( - self, - message: Optional[Union[List, Dict, str, Message]] = None, - console: Optional[Any] = None, - show_message: bool = True, - show_reasoning: bool = True, - show_full_reasoning: bool = False, - tags_to_include_in_markdown: Optional[Set[str]] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - markdown: bool = False, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> None: - import textwrap - - from rich.console import Group - from rich.json import JSON - from rich.live import Live - from rich.markdown import Markdown - from rich.status import Status - from rich.text import Text - - from agno.utils.response import format_tool_calls - - if not tags_to_include_in_markdown: - tags_to_include_in_markdown = {"think", "thinking"} - - with Live(console=console) as live_console: - status = Status( - "Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10 - ) - live_console.update(status) - - response_timer = Timer() - response_timer.start() - # Panels to be rendered - panels = [status] - # First render the message panel if the message is not None - if message and show_message: - # Convert message to a panel - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - panels.append(message_panel) - live_console.update(Group(*panels)) - - # Run the agent - run_response: TeamRunResponse = self.run( # type: ignore - message=message, - images=images, - audio=audio, - videos=videos, - files=files, - stream=False, - session_id=session_id, - user_id=user_id, - knowledge_filters=knowledge_filters, - **kwargs, - ) - response_timer.stop() - - team_markdown = False - member_markdown = {} - if markdown: - for member in self.members: - if isinstance(member, Agent) and member.agent_id is not None: - member_markdown[member.agent_id] = True - if isinstance(member, Team) and member.team_id is not None: - member_markdown[member.team_id] = True - team_markdown = True - - if self.response_model is not None: - team_markdown = False - - for member in self.members: - if ( - member.response_model is not None - and isinstance(member, Agent) - and member.agent_id is not None - ): - member_markdown[member.agent_id] = False # type: ignore - if ( - member.response_model is not None - and isinstance(member, Team) - and member.team_id is not None - ): - member_markdown[member.team_id] = False # type: ignore - - # Handle reasoning - reasoning_steps = [] - if ( - isinstance(run_response, TeamRunResponse) - and run_response.extra_data is not None - and run_response.extra_data.reasoning_steps is not None - ): - reasoning_steps = run_response.extra_data.reasoning_steps - - if len(reasoning_steps) > 0 and show_reasoning: - # Create panels for reasoning steps - for i, step in enumerate(reasoning_steps, 1): - reasoning_panel = self._build_reasoning_step_panel( - i, step, show_full_reasoning - ) - panels.append(reasoning_panel) - live_console.update(Group(*panels)) - - if ( - isinstance(run_response, TeamRunResponse) - and run_response.thinking is not None - ): - # Create panel for thinking - thinking_panel = create_panel( - content=Text(run_response.thinking), - title=f"Thinking ({response_timer.elapsed:.1f}s)", - border_style="green", - ) - panels.append(thinking_panel) - live_console.update(Group(*panels)) - - if isinstance(run_response, TeamRunResponse): - # Handle member responses - if self.show_members_responses: - for member_response in run_response.member_responses: - # Handle member reasoning - reasoning_steps = [] - if ( - isinstance(member_response, RunResponse) - and member_response.extra_data is not None - and member_response.extra_data.reasoning_steps is not None - ): - reasoning_steps.extend( - member_response.extra_data.reasoning_steps - ) - - if len(reasoning_steps) > 0 and show_reasoning: - # Create panels for reasoning steps - for i, step in enumerate(reasoning_steps, 1): - member_reasoning_panel = ( - self._build_reasoning_step_panel( - i, step, show_full_reasoning, color="magenta" - ) - ) - panels.append(member_reasoning_panel) - - # Add tool calls panel for member if available - if ( - self.show_tool_calls - and hasattr(member_response, "tools") - and member_response.tools - ): - member_name = None - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_name = self._get_member_name( - member_response.agent_id - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_name = self._get_member_name( - member_response.team_id - ) - - if member_name: - formatted_calls = format_tool_calls( - member_response.tools - ) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - for call in formatted_calls: - wrapped_call = textwrap.fill( - f"• {call}", - width=panel_width, - subsequent_indent=" ", - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - member_tool_calls_panel = create_panel( - content=tool_calls_text, - title=f"{member_name} Tool Calls", - border_style="yellow", - ) - panels.append(member_tool_calls_panel) - live_console.update(Group(*panels)) - - show_markdown = False - if member_markdown: - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - show_markdown = member_markdown.get( - member_response.agent_id, False - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - show_markdown = member_markdown.get( - member_response.team_id, False - ) - - member_response_content: Union[str, JSON, Markdown] = ( - self._parse_response_content( - member_response, - tags_to_include_in_markdown, - show_markdown=show_markdown, - ) - ) - - # Create panel for member response - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_response_panel = create_panel( - content=member_response_content, - title=f"{self._get_member_name(member_response.agent_id)} Response", - border_style="magenta", - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_response_panel = create_panel( - content=member_response_content, - title=f"{self._get_member_name(member_response.team_id)} Response", - border_style="magenta", - ) - panels.append(member_response_panel) - - if ( - member_response.citations is not None - and member_response.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate( - member_response.citations.urls - ) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="magenta", - ) - panels.append(citations_panel) - - live_console.update(Group(*panels)) - - # Add team level tool calls panel if available - if self.show_tool_calls and run_response.tools: - formatted_calls = format_tool_calls(run_response.tools) - if formatted_calls: - console_width = console.width if console else 80 - # Allow for panel borders and padding - panel_width = console_width + 30 - - lines = [] - for call in formatted_calls: - wrapped_call = textwrap.fill( - f"• {call}", width=panel_width, subsequent_indent=" " - ) # Indent continuation lines - lines.append(wrapped_call) - - # Join with blank lines between items - tool_calls_text = "\n\n".join(lines) - - team_tool_calls_panel = create_panel( - content=tool_calls_text, - title="Team Tool Calls", - border_style="yellow", - ) - panels.append(team_tool_calls_panel) - live_console.update(Group(*panels)) - - response_content_batch: Union[str, JSON, Markdown] = ( - self._parse_response_content( - run_response, - tags_to_include_in_markdown, - show_markdown=team_markdown, - ) - ) - - # Create panel for response - response_panel = create_panel( - content=response_content_batch, - title=f"Response ({response_timer.elapsed:.1f}s)", - border_style="blue", - ) - panels.append(response_panel) - - # Add citations - if ( - run_response.citations is not None - and run_response.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(run_response.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="green", - ) - panels.append(citations_panel) - - if self.memory is not None and isinstance(self.memory, Memory): - if ( - self.memory.memory_manager is not None - and self.memory.memory_manager.memories_updated - ): - memory_panel = create_panel( - content=Text("Memories updated"), - title="Memories", - border_style="green", - ) - panels.append(memory_panel) - - if ( - self.memory.summary_manager is not None - and self.memory.summary_manager.summary_updated - ): - summary_panel = create_panel( - content=Text("Session summary updated"), - title="Session Summary", - border_style="green", - ) - panels.append(summary_panel) - - # Final update to remove the "Thinking..." status - panels = [p for p in panels if not isinstance(p, Status)] - live_console.update(Group(*panels)) - - def _print_response_stream( - self, - message: Optional[Union[List, Dict, str, Message]] = None, - console: Optional[Any] = None, - show_message: bool = True, - show_reasoning: bool = True, - show_full_reasoning: bool = False, - tags_to_include_in_markdown: Optional[Set[str]] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - markdown: bool = False, - stream_intermediate_steps: bool = False, # type: ignore - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> None: - import textwrap - - from rich.console import Group - from rich.live import Live - from rich.markdown import Markdown - from rich.status import Status - from rich.text import Text - - from agno.utils.response import format_tool_calls - - if not tags_to_include_in_markdown: - tags_to_include_in_markdown = {"think", "thinking"} - - stream_intermediate_steps = ( - True # With streaming print response, we need to stream intermediate steps - ) - - _response_content: str = "" - _response_thinking: str = "" - reasoning_steps: List[ReasoningStep] = [] - - # Track tool calls by member and team - member_tool_calls = {} # type: ignore - team_tool_calls = [] # type: ignore - - # Track processed tool calls to avoid duplicates - processed_tool_calls = set() - - with Live(console=console) as live_console: - status = Status( - "Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10 - ) - live_console.update(status) - response_timer = Timer() - response_timer.start() - # Flag which indicates if the panels should be rendered - render = False - # Panels to be rendered - panels = [status] - # First render the message panel if the message is not None - if message and show_message: - render = True - # Convert message to a panel - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - panels.append(message_panel) - if render: - live_console.update(Group(*panels)) - - # Get response from the team - stream_resp = self.run( # type: ignore - message=message, - audio=audio, - images=images, - videos=videos, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - session_id=session_id, - user_id=user_id, - knowledge_filters=knowledge_filters, - **kwargs, - ) - - team_markdown = None - member_markdown = {} - - # Dict to track member response panels by member_id - member_response_panels = {} - - for resp in stream_resp: - if team_markdown is None: - if markdown: - team_markdown = True - else: - team_markdown = False - - if self.response_model is not None: - team_markdown = False - - if isinstance(resp, tuple(get_args(TeamRunResponseEvent))): - if resp.event == TeamRunEvent.run_response_content: - if isinstance(resp.content, str): - _response_content += resp.content - if resp.thinking is not None: - _response_thinking += resp.thinking - if ( - hasattr(resp, "extra_data") - and resp.extra_data is not None - and resp.extra_data.reasoning_steps is not None - ): - reasoning_steps = resp.extra_data.reasoning_steps - - # Collect team tool calls, avoiding duplicates - if ( - self.show_tool_calls - and isinstance(resp, ToolCallCompletedEvent) - and resp.tool - ): - tool = resp.tool - # Generate a unique ID for this tool call - if tool.tool_call_id: - tool_id = tool.tool_call_id - else: - tool_id = str(hash(str(tool))) - if tool_id not in processed_tool_calls: - processed_tool_calls.add(tool_id) - team_tool_calls.append(tool) - - # Collect member tool calls, avoiding duplicates - if ( - self.show_tool_calls - and hasattr(resp, "member_responses") - and resp.member_responses - ): - for member_response in resp.member_responses: - member_id = None - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_id = member_response.agent_id - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_id = member_response.team_id - - if ( - member_id - and hasattr(member_response, "tools") - and member_response.tools - ): - if member_id not in member_tool_calls: - member_tool_calls[member_id] = [] - - for tool in member_response.tools: - # Generate a unique ID for this tool call - if tool.tool_call_id: - tool_id = tool.tool_call_id - else: - tool_id = str(hash(str(tool))) - if tool_id not in processed_tool_calls: - processed_tool_calls.add(tool_id) - member_tool_calls[member_id].append(tool) - - response_content_stream: Union[str, Markdown] = _response_content - # Escape special tags before markdown conversion - if team_markdown: - escaped_content = escape_markdown_tags( - _response_content, tags_to_include_in_markdown - ) - response_content_stream = Markdown(escaped_content) - - # Create new panels for each chunk - panels = [] - - if message and show_message: - render = True - # Convert message to a panel - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - panels.append(message_panel) - - if len(reasoning_steps) > 0 and show_reasoning: - render = True - # Create panels for reasoning steps - for i, step in enumerate(reasoning_steps, 1): - reasoning_panel = self._build_reasoning_step_panel( - i, step, show_full_reasoning - ) - panels.append(reasoning_panel) - - if len(_response_thinking) > 0: - render = True - # Create panel for thinking - thinking_panel = create_panel( - content=Text(_response_thinking), - title=f"Thinking ({response_timer.elapsed:.1f}s)", - border_style="green", - ) - panels.append(thinking_panel) - elif _response_content == "": - # Keep showing status if no content yet - panels.append(status) - - # Process member responses and their tool calls - for member_response in ( - resp.member_responses if hasattr(resp, "member_responses") else [] - ): - member_id = None - member_name = "Team Member" - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_id = member_response.agent_id - member_name = self._get_member_name(member_id) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_id = member_response.team_id - member_name = self._get_member_name(member_id) - - # If we have tool calls for this member, display them - if ( - self.show_tool_calls - and member_id in member_tool_calls - and member_tool_calls[member_id] - ): - formatted_calls = format_tool_calls( - member_tool_calls[member_id] - ) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - for call in formatted_calls: - wrapped_call = textwrap.fill( - f"• {call}", - width=panel_width, - subsequent_indent=" ", - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - member_tool_calls_panel = create_panel( - content=tool_calls_text, - title=f"{member_name} Tool Calls", - border_style="yellow", - ) - panels.append(member_tool_calls_panel) - - # Process member response content - if self.show_members_responses and member_id is not None: - show_markdown = False - if markdown: - show_markdown = True - - member_response_content = self._parse_response_content( - member_response, - tags_to_include_in_markdown, - show_markdown=show_markdown, - ) - - member_response_panel = create_panel( - content=member_response_content, - title=f"{member_name} Response", - border_style="magenta", - ) - - panels.append(member_response_panel) - - # Store for reference - if member_id is not None: - member_response_panels[member_id] = member_response_panel - - # Add team tool calls panel if available (before the team response) - if self.show_tool_calls and team_tool_calls: - formatted_calls = format_tool_calls(team_tool_calls) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - # Create a set to track already added calls by their string representation - added_calls = set() - for call in formatted_calls: - if call not in added_calls: - added_calls.add(call) - # Wrap the call text to fit within the panel - wrapped_call = textwrap.fill( - f"• {call}", - width=panel_width, - subsequent_indent=" ", - ) - lines.append(wrapped_call) - - # Join with blank lines between items - tool_calls_text = "\n\n".join(lines) - - team_tool_calls_panel = create_panel( - content=tool_calls_text, - title="Team Tool Calls", - border_style="yellow", - ) - panels.append(team_tool_calls_panel) - - # Add the team response panel at the end - if len(_response_content) > 0: - render = True - # Create panel for response - response_panel = create_panel( - content=response_content_stream, - title=f"Response ({response_timer.elapsed:.1f}s)", - border_style="blue", - ) - panels.append(response_panel) - - if render or len(panels) > 0: - live_console.update(Group(*panels)) - - response_timer.stop() - - # Add citations - if ( - hasattr(resp, "citations") - and resp.citations is not None - and resp.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(resp.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="green", - ) - panels.append(citations_panel) - live_console.update(Group(*panels)) - - if self.memory is not None and isinstance(self.memory, Memory): - if ( - self.memory.memory_manager is not None - and self.memory.memory_manager.memories_updated - ): - memory_panel = create_panel( - content=Text("Memories updated"), - title="Memories", - border_style="green", - ) - panels.append(memory_panel) - live_console.update(Group(*panels)) - - if ( - self.memory.summary_manager is not None - and self.memory.summary_manager.summary_updated - ): - summary_panel = create_panel( - content=Text("Session summary updated"), - title="Session Summary", - border_style="green", - ) - panels.append(summary_panel) - live_console.update(Group(*panels)) - - # Final update to remove the "Thinking..." status - panels = [p for p in panels if not isinstance(p, Status)] - - if markdown: - for member in self.members: - if isinstance(member, Agent) and member.agent_id is not None: - member_markdown[member.agent_id] = True - if isinstance(member, Team) and member.team_id is not None: - member_markdown[member.team_id] = True - - for member in self.members: - if ( - member.response_model is not None - and isinstance(member, Agent) - and member.agent_id is not None - ): - member_markdown[member.agent_id] = False # type: ignore - if ( - member.response_model is not None - and isinstance(member, Team) - and member.team_id is not None - ): - member_markdown[member.team_id] = False # type: ignore - - # Final panels assembly - we'll recreate the panels from scratch to ensure correct order - final_panels = [] - - # Start with the message - if message and show_message: - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - final_panels.append(message_panel) - - # Add reasoning steps - if reasoning_steps and show_reasoning: - for i, step in enumerate(reasoning_steps, 1): - reasoning_panel = self._build_reasoning_step_panel( - i, step, show_full_reasoning - ) - final_panels.append(reasoning_panel) - - # Add thinking panel if available - if _response_thinking: - thinking_panel = create_panel( - content=Text(_response_thinking), - title=f"Thinking ({response_timer.elapsed:.1f}s)", - border_style="green", - ) - final_panels.append(thinking_panel) - - # Add member tool calls and responses in correct order - for i, member_response in enumerate( - self.run_response.member_responses if self.run_response else [] - ): - member_id = None - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_id = member_response.agent_id - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_id = member_response.team_id - - if member_id: - # First add tool calls if any - if ( - self.show_tool_calls - and member_id in member_tool_calls - and member_tool_calls[member_id] - ): - formatted_calls = format_tool_calls( - member_tool_calls[member_id] - ) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - for call in formatted_calls: - wrapped_call = textwrap.fill( - f"• {call}", - width=panel_width, - subsequent_indent=" ", - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - member_name = self._get_member_name(member_id) - member_tool_calls_panel = create_panel( - content=tool_calls_text, - title=f"{member_name} Tool Calls", - border_style="yellow", - ) - final_panels.append(member_tool_calls_panel) - - # Add reasoning steps if any - reasoning_steps = [] - if ( - member_response.extra_data is not None - and member_response.extra_data.reasoning_steps is not None - ): - reasoning_steps = member_response.extra_data.reasoning_steps - if reasoning_steps and show_reasoning: - for j, step in enumerate(reasoning_steps, 1): - member_reasoning_panel = self._build_reasoning_step_panel( - j, step, show_full_reasoning, color="magenta" - ) - final_panels.append(member_reasoning_panel) - - # Then add response - show_markdown = False - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - show_markdown = member_markdown.get( - member_response.agent_id, False - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - show_markdown = member_markdown.get( - member_response.team_id, False - ) - - member_response_content = self._parse_response_content( - member_response, - tags_to_include_in_markdown, - show_markdown=show_markdown, - ) - - member_name = "Team Member" - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_name = self._get_member_name(member_response.agent_id) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_name = self._get_member_name(member_response.team_id) - - member_response_panel = create_panel( - content=member_response_content, - title=f"{member_name} Response", - border_style="magenta", - ) - final_panels.append(member_response_panel) - - # Add citations if any - if ( - member_response.citations is not None - and member_response.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(member_response.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="magenta", - ) - final_panels.append(citations_panel) - - # Add team tool calls before team response - if self.show_tool_calls and team_tool_calls: - formatted_calls = format_tool_calls(team_tool_calls) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - # Create a set to track already added calls by their string representation - added_calls = set() - for call in formatted_calls: - if call not in added_calls: - added_calls.add(call) - # Wrap the call text to fit within the panel - wrapped_call = textwrap.fill( - f"• {call}", width=panel_width, subsequent_indent=" " - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - team_tool_calls_panel = create_panel( - content=tool_calls_text, - title="Team Tool Calls", - border_style="yellow", - ) - final_panels.append(team_tool_calls_panel) - - # Add team response - if _response_content: - response_content_stream = _response_content - if team_markdown: - escaped_content = escape_markdown_tags( - _response_content, tags_to_include_in_markdown - ) - response_content_stream = Markdown(escaped_content) - - response_panel = create_panel( - content=response_content_stream, - title=f"Response ({response_timer.elapsed:.1f}s)", - border_style="blue", - ) - final_panels.append(response_panel) - - # Add team citations - if ( - hasattr(resp, "citations") - and resp.citations is not None - and resp.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(resp.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="green", - ) - final_panels.append(citations_panel) - - # Final update with correctly ordered panels - live_console.update(Group(*final_panels)) - - async def aprint_response( - self, - message: Optional[Union[List, Dict, str, Message]] = None, - *, - stream: bool = False, - stream_intermediate_steps: bool = False, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - show_message: bool = True, - show_reasoning: bool = True, - show_full_reasoning: bool = False, - console: Optional[Any] = None, - tags_to_include_in_markdown: Optional[Set[str]] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - markdown: Optional[bool] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> None: - if not tags_to_include_in_markdown: - tags_to_include_in_markdown = {"think", "thinking"} - - if markdown is None: - markdown = self.markdown - else: - self.markdown = markdown - - if self.response_model is not None: - stream = False - - if stream: - await self._aprint_response_stream( - message=message, - console=console, - show_message=show_message, - show_reasoning=show_reasoning, - show_full_reasoning=show_full_reasoning, - tags_to_include_in_markdown=tags_to_include_in_markdown, - session_id=session_id, - user_id=user_id, - audio=audio, - images=images, - videos=videos, - files=files, - markdown=markdown, - stream_intermediate_steps=stream_intermediate_steps, - knowledge_filters=knowledge_filters, - **kwargs, - ) - else: - await self._aprint_response( - message=message, - console=console, - show_message=show_message, - show_reasoning=show_reasoning, - show_full_reasoning=show_full_reasoning, - tags_to_include_in_markdown=tags_to_include_in_markdown, - session_id=session_id, - user_id=user_id, - audio=audio, - images=images, - videos=videos, - files=files, - markdown=markdown, - knowledge_filters=knowledge_filters, - **kwargs, - ) - - async def _aprint_response( - self, - message: Optional[Union[List, Dict, str, Message]] = None, - console: Optional[Any] = None, - show_message: bool = True, - show_reasoning: bool = True, - show_full_reasoning: bool = False, - tags_to_include_in_markdown: Optional[Set[str]] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - markdown: bool = False, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> None: - import textwrap - - from rich.console import Group - from rich.json import JSON - from rich.live import Live - from rich.markdown import Markdown - from rich.status import Status - from rich.text import Text - - from agno.utils.response import format_tool_calls - - if not tags_to_include_in_markdown: - tags_to_include_in_markdown = {"think", "thinking"} - - with Live(console=console) as live_console: - status = Status( - "Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10 - ) - live_console.update(status) - - response_timer = Timer() - response_timer.start() - # Panels to be rendered - panels = [status] - # First render the message panel if the message is not None - if message and show_message: - # Convert message to a panel - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - panels.append(message_panel) - live_console.update(Group(*panels)) - - # Run the agent - run_response: TeamRunResponse = await self.arun( # type: ignore - message=message, - images=images, - audio=audio, - videos=videos, - files=files, - stream=False, - session_id=session_id, - user_id=user_id, - knowledge_filters=knowledge_filters, - **kwargs, - ) - response_timer.stop() - - team_markdown = False - member_markdown = {} - if markdown: - for member in self.members: - if isinstance(member, Agent) and member.agent_id is not None: - member_markdown[member.agent_id] = True - if isinstance(member, Team) and member.team_id is not None: - member_markdown[member.team_id] = True - team_markdown = True - - if self.response_model is not None: - team_markdown = False - - for member in self.members: - if ( - member.response_model is not None - and isinstance(member, Agent) - and member.agent_id is not None - ): - member_markdown[member.agent_id] = False # type: ignore - if ( - member.response_model is not None - and isinstance(member, Team) - and member.team_id is not None - ): - member_markdown[member.team_id] = False # type: ignore - - # Handle reasoning - reasoning_steps = [] - if ( - isinstance(run_response, TeamRunResponse) - and run_response.extra_data is not None - and run_response.extra_data.reasoning_steps is not None - ): - reasoning_steps = run_response.extra_data.reasoning_steps - - if len(reasoning_steps) > 0 and show_reasoning: - # Create panels for reasoning steps - for i, step in enumerate(reasoning_steps, 1): - reasoning_panel = self._build_reasoning_step_panel( - i, step, show_full_reasoning - ) - panels.append(reasoning_panel) - live_console.update(Group(*panels)) - - if ( - isinstance(run_response, TeamRunResponse) - and run_response.thinking is not None - ): - # Create panel for thinking - thinking_panel = create_panel( - content=Text(run_response.thinking), - title=f"Thinking ({response_timer.elapsed:.1f}s)", - border_style="green", - ) - panels.append(thinking_panel) - live_console.update(Group(*panels)) - - if isinstance(run_response, TeamRunResponse): - # Handle member responses - if self.show_members_responses: - for member_response in run_response.member_responses: - # Handle member reasoning - reasoning_steps = [] - if ( - isinstance(member_response, RunResponse) - and member_response.extra_data is not None - and member_response.extra_data.reasoning_steps is not None - ): - reasoning_steps.extend( - member_response.extra_data.reasoning_steps - ) - - if len(reasoning_steps) > 0 and show_reasoning: - # Create panels for reasoning steps - for i, step in enumerate(reasoning_steps, 1): - member_reasoning_panel = ( - self._build_reasoning_step_panel( - i, step, show_full_reasoning, color="magenta" - ) - ) - panels.append(member_reasoning_panel) - - # Add tool calls panel for member if available - if ( - self.show_tool_calls - and hasattr(member_response, "tools") - and member_response.tools - ): - member_name = None - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_name = self._get_member_name( - member_response.agent_id - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_name = self._get_member_name( - member_response.team_id - ) - - if member_name: - # Format tool calls - formatted_calls = format_tool_calls( - member_response.tools - ) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - for call in formatted_calls: - wrapped_call = textwrap.fill( - f"• {call}", - width=panel_width, - subsequent_indent=" ", - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - member_tool_calls_panel = create_panel( - content=tool_calls_text, - title=f"{member_name} Tool Calls", - border_style="yellow", - ) - panels.append(member_tool_calls_panel) - live_console.update(Group(*panels)) - - show_markdown = False - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - show_markdown = member_markdown.get( - member_response.agent_id, False - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - show_markdown = member_markdown.get( - member_response.team_id, False - ) - - member_response_content: Union[str, JSON, Markdown] = ( - self._parse_response_content( - member_response, - tags_to_include_in_markdown, - show_markdown=show_markdown, - ) - ) - - # Create panel for member response - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_response_panel = create_panel( - content=member_response_content, - title=f"{self._get_member_name(member_response.agent_id)} Response", - border_style="magenta", - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_response_panel = create_panel( - content=member_response_content, - title=f"{self._get_member_name(member_response.team_id)} Response", - border_style="magenta", - ) - panels.append(member_response_panel) - - if ( - member_response.citations is not None - and member_response.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate( - member_response.citations.urls - ) - if citation.url # Only include citations with valid URLs - ) - if md_content: - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="magenta", - ) - panels.append(citations_panel) - - live_console.update(Group(*panels)) - - # Add team level tool calls panel if available - if self.show_tool_calls and run_response.tools: - formatted_calls = format_tool_calls(run_response.tools) - if formatted_calls: - console_width = console.width if console else 80 - # Allow for panel borders and padding - panel_width = console_width + 30 - - lines = [] - for call in formatted_calls: - # Wrap the call text to fit within the panel - wrapped_call = textwrap.fill( - f"• {call}", width=panel_width, subsequent_indent=" " - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - team_tool_calls_panel = create_panel( - content=tool_calls_text, - title="Team Tool Calls", - border_style="yellow", - ) - panels.append(team_tool_calls_panel) - live_console.update(Group(*panels)) - - response_content_batch: Union[str, JSON, Markdown] = ( - self._parse_response_content( - run_response, - tags_to_include_in_markdown, - show_markdown=team_markdown, - ) - ) - - # Create panel for response - response_panel = create_panel( - content=response_content_batch, - title=f"Response ({response_timer.elapsed:.1f}s)", - border_style="blue", - ) - panels.append(response_panel) - - # Add citations - if ( - run_response.citations is not None - and run_response.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(run_response.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="green", - ) - panels.append(citations_panel) - - if self.memory is not None and isinstance(self.memory, Memory): - if ( - self.memory.memory_manager is not None - and self.memory.memory_manager.memories_updated - ): - memory_panel = create_panel( - content=Text("Memories updated"), - title="Memories", - border_style="green", - ) - panels.append(memory_panel) - - if ( - self.memory.summary_manager is not None - and self.memory.summary_manager.summary_updated - ): - summary_panel = create_panel( - content=Text("Session summary updated"), - title="Session Summary", - border_style="green", - ) - panels.append(summary_panel) - - # Final update to remove the "Thinking..." status - panels = [p for p in panels if not isinstance(p, Status)] - live_console.update(Group(*panels)) - - async def _aprint_response_stream( - self, - message: Optional[Union[List, Dict, str, Message]] = None, - console: Optional[Any] = None, - show_message: bool = True, - show_reasoning: bool = True, - show_full_reasoning: bool = False, - tags_to_include_in_markdown: Optional[Set[str]] = None, - session_id: Optional[str] = None, - user_id: Optional[str] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - markdown: bool = False, - stream_intermediate_steps: bool = False, # type: ignore - **kwargs: Any, - ) -> None: - import textwrap - - from rich.console import Group - from rich.live import Live - from rich.markdown import Markdown - from rich.status import Status - from rich.text import Text - - if not tags_to_include_in_markdown: - tags_to_include_in_markdown = {"think", "thinking"} - - stream_intermediate_steps = ( - True # With streaming print response, we need to stream intermediate steps - ) - - self.run_response = cast(TeamRunResponse, self.run_response) - - _response_content: str = "" - _response_thinking: str = "" - reasoning_steps: List[ReasoningStep] = [] - - # Track tool calls by member and team - member_tool_calls = {} # type: ignore - team_tool_calls: List[ToolExecution] = [] - - # Track processed tool calls to avoid duplicates - processed_tool_calls = set() - - # Initialize final_panels here - final_panels = [] # type: ignore - - with Live(console=console) as live_console: - status = Status( - "Thinking...", spinner="aesthetic", speed=0.4, refresh_per_second=10 - ) - live_console.update(status) - response_timer = Timer() - response_timer.start() - # Flag which indicates if the panels should be rendered - render = False - # Panels to be rendered - panels = [status] - # First render the message panel if the message is not None - if message and show_message: - render = True - # Convert message to a panel - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - panels.append(message_panel) - if render: - live_console.update(Group(*panels)) - - # Get response from the team - stream_resp = await self.arun( # type: ignore - message=message, - audio=audio, - images=images, - videos=videos, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - session_id=session_id, - user_id=user_id, - **kwargs, - ) - team_markdown = None - member_markdown = {} - - async for resp in stream_resp: - if team_markdown is None: - if markdown: - team_markdown = True - else: - team_markdown = False - - if self.response_model is not None: - team_markdown = False - - if isinstance(resp, tuple(get_args(TeamRunResponseEvent))): - if resp.event == TeamRunEvent.run_response_content: - if isinstance(resp.content, str): - _response_content += resp.content - if resp.thinking is not None: - _response_thinking += resp.thinking - if ( - hasattr(resp, "extra_data") - and resp.extra_data is not None - and resp.extra_data.reasoning_steps is not None - ): - reasoning_steps = resp.extra_data.reasoning_steps - - # Collect team tool calls, avoiding duplicates - if ( - self.show_tool_calls - and isinstance(resp, ToolCallCompletedEvent) - and resp.tool - ): - tool = resp.tool - # Generate a unique ID for this tool call - if tool.tool_call_id is not None: - tool_id = tool.tool_call_id - else: - tool_id = str(hash(str(tool))) - if tool_id not in processed_tool_calls: - processed_tool_calls.add(tool_id) - team_tool_calls.append(tool) - - # Collect member tool calls, avoiding duplicates - if ( - self.show_tool_calls - and hasattr(resp, "member_responses") - and resp.member_responses - ): - for member_response in resp.member_responses: - member_id = None - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_id = member_response.agent_id - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_id = member_response.team_id - - if ( - member_id - and hasattr(member_response, "tools") - and member_response.tools - ): - if member_id not in member_tool_calls: - member_tool_calls[member_id] = [] - - for tool in member_response.tools: - if tool.tool_call_id is not None: - tool_id = tool.tool_call_id - else: - tool_id = str(hash(str(tool))) - if tool_id not in processed_tool_calls: - processed_tool_calls.add(tool_id) - member_tool_calls[member_id].append(tool) - - response_content_stream: Union[str, Markdown] = _response_content - # Escape special tags before markdown conversion - if team_markdown: - escaped_content = escape_markdown_tags( - _response_content, tags_to_include_in_markdown - ) - response_content_stream = Markdown(escaped_content) - - # Create new panels for each chunk - panels = [status] - - if message and show_message: - render = True - # Convert message to a panel - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - panels.append(message_panel) - if render: - live_console.update(Group(*panels)) - - if len(reasoning_steps) > 0 and show_reasoning: - render = True - # Create panels for reasoning steps - for i, step in enumerate(reasoning_steps, 1): - reasoning_panel = self._build_reasoning_step_panel( - i, step, show_full_reasoning - ) - panels.append(reasoning_panel) - if render: - live_console.update(Group(*panels)) - - if len(_response_thinking) > 0: - render = True - # Create panel for thinking - thinking_panel = create_panel( - content=Text(_response_thinking), - title=f"Thinking ({response_timer.elapsed:.1f}s)", - border_style="green", - ) - panels.append(thinking_panel) - if render: - live_console.update(Group(*panels)) - - # Add tool calls panel if available - if ( - self.show_tool_calls - and resp is not None - and self.run_response.formatted_tool_calls - ): - render = True - # Create bullet points for each tool call - tool_calls_content = Text() - # Use a set to track already processed tool calls - added_tool_calls = set() - for tool_call in self.run_response.formatted_tool_calls: - if tool_call not in added_tool_calls: - added_tool_calls.add(tool_call) - tool_calls_content.append(f"• {tool_call}\n") - - tool_calls_panel = create_panel( - content=tool_calls_content.plain.rstrip(), - title="Tool Calls", - border_style="yellow", - ) - panels.append(tool_calls_panel) - - if len(_response_content) > 0: - render = True - # Create panel for response - response_panel = create_panel( - content=response_content_stream, - title=f"Response ({response_timer.elapsed:.1f}s)", - border_style="blue", - ) - panels.append(response_panel) - if render: - live_console.update(Group(*panels)) - response_timer.stop() - - # Add citations - if ( - hasattr(resp, "citations") - and resp.citations is not None - and resp.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(resp.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="green", - ) - panels.append(citations_panel) - live_console.update(Group(*panels)) - - if self.memory is not None and isinstance(self.memory, Memory): - if ( - self.memory.memory_manager is not None - and self.memory.memory_manager.memories_updated - ): - memory_panel = create_panel( - content=Text("Memories updated"), - title="Memories", - border_style="green", - ) - panels.append(memory_panel) - live_console.update(Group(*panels)) - - if ( - self.memory.summary_manager is not None - and self.memory.summary_manager.summary_updated - ): - summary_panel = create_panel( - content=Text("Session summary updated"), - title="Session Summary", - border_style="green", - ) - panels.append(summary_panel) - live_console.update(Group(*panels)) - - # Final update to remove the "Thinking..." status - panels = [p for p in panels if not isinstance(p, Status)] - - if markdown: - for member in self.members: - if isinstance(member, Agent) and member.agent_id is not None: - member_markdown[member.agent_id] = True # type: ignore - if isinstance(member, Team) and member.team_id is not None: - member_markdown[member.team_id] = True # type: ignore - - for member in self.members: - if ( - member.response_model is not None - and isinstance(member, Agent) - and member.agent_id is not None - ): - member_markdown[member.agent_id] = False # type: ignore - if ( - member.response_model is not None - and isinstance(member, Team) - and member.team_id is not None - ): - member_markdown[member.team_id] = False # type: ignore - - # Final panels assembly - we'll recreate the panels from scratch to ensure correct order - final_panels = [] - - # Start with the message - if message and show_message: - message_content = get_text_from_message(message) - message_panel = create_panel( - content=Text(message_content, style="green"), - title="Message", - border_style="cyan", - ) - final_panels.append(message_panel) - - # Add reasoning steps - if reasoning_steps and show_reasoning: - for i, step in enumerate(reasoning_steps, 1): - reasoning_panel = self._build_reasoning_step_panel( - i, step, show_full_reasoning - ) - final_panels.append(reasoning_panel) - - # Add thinking panel if available - if _response_thinking: - thinking_panel = create_panel( - content=Text(_response_thinking), - title=f"Thinking ({response_timer.elapsed:.1f}s)", - border_style="green", - ) - final_panels.append(thinking_panel) - - # Add member tool calls and responses in correct order - for i, member_response in enumerate( - self.run_response.member_responses if self.run_response else [] - ): - member_id = None - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_id = member_response.agent_id - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_id = member_response.team_id - - if member_id: - # First add tool calls if any - if ( - self.show_tool_calls - and member_id in member_tool_calls - and member_tool_calls[member_id] - ): - formatted_calls = format_tool_calls( - member_tool_calls[member_id] - ) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - # Create a set to track already added calls by their string representation - added_calls = set() - for call in formatted_calls: - if call not in added_calls: - added_calls.add(call) - # Wrap the call text to fit within the panel - wrapped_call = textwrap.fill( - f"• {call}", - width=panel_width, - subsequent_indent=" ", - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - member_name = self._get_member_name(member_id) - member_tool_calls_panel = create_panel( - content=tool_calls_text, - title=f"{member_name} Tool Calls", - border_style="yellow", - ) - final_panels.append(member_tool_calls_panel) - - # Add reasoning steps if any - reasoning_steps = [] - if ( - member_response.extra_data is not None - and member_response.extra_data.reasoning_steps is not None - ): - reasoning_steps = member_response.extra_data.reasoning_steps - if reasoning_steps and show_reasoning: - for j, step in enumerate(reasoning_steps, 1): - member_reasoning_panel = self._build_reasoning_step_panel( - j, step, show_full_reasoning, color="magenta" - ) - final_panels.append(member_reasoning_panel) - - # Then add response - show_markdown = False - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - show_markdown = member_markdown.get( - member_response.agent_id, False - ) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - show_markdown = member_markdown.get( - member_response.team_id, False - ) - - member_response_content = self._parse_response_content( - member_response, - tags_to_include_in_markdown, - show_markdown=show_markdown, - ) - - member_name = "Team Member" - if ( - isinstance(member_response, RunResponse) - and member_response.agent_id is not None - ): - member_name = self._get_member_name(member_response.agent_id) - elif ( - isinstance(member_response, TeamRunResponse) - and member_response.team_id is not None - ): - member_name = self._get_member_name(member_response.team_id) - - member_response_panel = create_panel( - content=member_response_content, - title=f"{member_name} Response", - border_style="magenta", - ) - final_panels.append(member_response_panel) - - # Add citations if any - if ( - member_response.citations is not None - and member_response.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(member_response.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="magenta", - ) - final_panels.append(citations_panel) - - # Add team tool calls before team response - if self.show_tool_calls and team_tool_calls: - formatted_calls = format_tool_calls(team_tool_calls) - if formatted_calls: - console_width = console.width if console else 80 - panel_width = console_width + 30 - - lines = [] - # Create a set to track already added calls by their string representation - added_calls = set() - for call in formatted_calls: - if call not in added_calls: - added_calls.add(call) - # Wrap the call text to fit within the panel - wrapped_call = textwrap.fill( - f"• {call}", width=panel_width, subsequent_indent=" " - ) - lines.append(wrapped_call) - - tool_calls_text = "\n\n".join(lines) - - team_tool_calls_panel = create_panel( - content=tool_calls_text, - title="Team Tool Calls", - border_style="yellow", - ) - final_panels.append(team_tool_calls_panel) - - # Add team response - if _response_content: - response_content_stream = _response_content - if team_markdown: - escaped_content = escape_markdown_tags( - _response_content, tags_to_include_in_markdown - ) - response_content_stream = Markdown(escaped_content) - - response_panel = create_panel( - content=response_content_stream, - title=f"Response ({response_timer.elapsed:.1f}s)", - border_style="blue", - ) - final_panels.append(response_panel) - - # Add team citations - if ( - hasattr(resp, "citations") - and resp.citations is not None - and resp.citations.urls is not None - ): - md_content = "\n".join( - f"{i + 1}. [{citation.title or citation.url}]({citation.url})" - for i, citation in enumerate(resp.citations.urls) - if citation.url # Only include citations with valid URLs - ) - if md_content: # Only create panel if there are citations - citations_panel = create_panel( - content=Markdown(md_content), - title="Citations", - border_style="green", - ) - final_panels.append(citations_panel) - - # Final update with correctly ordered panels - live_console.update(Group(*final_panels)) - - def _build_reasoning_step_panel( - self, - step_idx: int, - step: ReasoningStep, - show_full_reasoning: bool = False, - color: str = "green", - ): - from rich.text import Text - - # Build step content - step_content = Text.assemble() - if step.title is not None: - step_content.append(f"{step.title}\n", "bold") - if step.action is not None: - step_content.append( - Text.from_markup(f"[bold]Action:[/bold] {step.action}\n", style="dim") - ) - if step.result is not None: - step_content.append(Text.from_markup(step.result, style="dim")) - - if show_full_reasoning: - # Add detailed reasoning information if available - if step.reasoning is not None: - step_content.append( - Text.from_markup( - f"\n[bold]Reasoning:[/bold] {step.reasoning}", style="dim" - ) - ) - if step.confidence is not None: - step_content.append( - Text.from_markup( - f"\n[bold]Confidence:[/bold] {step.confidence}", style="dim" - ) - ) - return create_panel( - content=step_content, title=f"Reasoning step {step_idx}", border_style=color - ) - - def _get_member_name(self, entity_id: str) -> str: - for member in self.members: - if isinstance(member, Agent): - if member.agent_id == entity_id: - return member.name or entity_id - elif isinstance(member, Team): - if member.team_id == entity_id: - return member.name or entity_id - return entity_id - - def _parse_response_content( - self, - run_response: Union[TeamRunResponse, RunResponse], - tags_to_include_in_markdown: Set[str], - show_markdown: bool = True, - ) -> Any: - from rich.json import JSON - from rich.markdown import Markdown - - if isinstance(run_response.content, str): - if show_markdown: - escaped_content = escape_markdown_tags( - run_response.content, tags_to_include_in_markdown - ) - return Markdown(escaped_content) - else: - return run_response.get_content_as_string(indent=4) - elif isinstance(run_response.content, BaseModel): - try: - return JSON( - run_response.content.model_dump_json(exclude_none=True), indent=2 - ) - except Exception as e: - log_warning(f"Failed to convert response to JSON: {e}") - else: - try: - return JSON(json.dumps(run_response.content), indent=4) - except Exception as e: - log_warning(f"Failed to convert response to JSON: {e}") - - def cli_app( - self, - message: Optional[str] = None, - user: str = "User", - emoji: str = ":sunglasses:", - stream: bool = False, - markdown: bool = False, - exit_on: Optional[List[str]] = None, - **kwargs: Any, - ) -> None: - from rich.prompt import Prompt - - if message: - self.print_response( - message=message, stream=stream, markdown=markdown, **kwargs - ) - - _exit_on = exit_on or ["exit", "quit", "bye"] - while True: - message = Prompt.ask(f"[bold] {emoji} {user} [/bold]") - if message in _exit_on: - break - - self.print_response( - message=message, stream=stream, markdown=markdown, **kwargs - ) - - ########################################################################### - # Helpers - ########################################################################### - - def _handle_reasoning( - self, run_response: TeamRunResponse, run_messages: RunMessages - ) -> None: - if self.reasoning or self.reasoning_model is not None: - reasoning_generator = self._reason( - run_response=run_response, run_messages=run_messages - ) - - # Consume the generator without yielding - deque(reasoning_generator, maxlen=0) - - def _handle_reasoning_stream( - self, run_response: TeamRunResponse, run_messages: RunMessages - ) -> Iterator[TeamRunResponseEvent]: - if self.reasoning or self.reasoning_model is not None: - reasoning_generator = self._reason( - run_response=run_response, run_messages=run_messages - ) - yield from reasoning_generator - - async def _ahandle_reasoning( - self, run_response: TeamRunResponse, run_messages: RunMessages - ) -> None: - if self.reasoning or self.reasoning_model is not None: - reason_generator = self._areason( - run_response=run_response, run_messages=run_messages - ) - # Consume the generator without yielding - async for _ in reason_generator: - pass - - async def _ahandle_reasoning_stream( - self, run_response: TeamRunResponse, run_messages: RunMessages - ) -> AsyncIterator[TeamRunResponseEvent]: - if self.reasoning or self.reasoning_model is not None: - reason_generator = self._areason( - run_response=run_response, run_messages=run_messages - ) - async for item in reason_generator: - yield item - - def _calculate_session_metrics(self, messages: List[Message]) -> SessionMetrics: - session_metrics = SessionMetrics() - assistant_message_role = ( - self.model.assistant_message_role if self.model is not None else "assistant" - ) - - # Get metrics of the team-agent's messages - for m in messages: - if m.role == assistant_message_role and m.metrics is not None: - session_metrics += m.metrics - - return session_metrics - - def _calculate_full_team_session_metrics( - self, messages: List[Message] - ) -> SessionMetrics: - current_session_metrics = ( - self.session_metrics or self._calculate_session_metrics(messages) - ) - current_session_metrics = replace(current_session_metrics) - assistant_message_role = ( - self.model.assistant_message_role if self.model is not None else "assistant" - ) - - # Get metrics of the team-agent's messages - for member in self.members: - # Only members that ran has memory - if member.memory is not None: - if isinstance(member.memory, AgentMemory): - for m in member.memory.messages: - if m.role == assistant_message_role and m.metrics is not None: - current_session_metrics += m.metrics - return current_session_metrics - - def _aggregate_metrics_from_messages( - self, messages: List[Message] - ) -> Dict[str, Any]: - aggregated_metrics: Dict[str, Any] = defaultdict(list) - assistant_message_role = ( - self.model.assistant_message_role if self.model is not None else "assistant" - ) - for m in messages: - if m.role == assistant_message_role and m.metrics is not None: - for k, v in asdict(m.metrics).items(): # type: ignore - if k == "timer": - continue - if v is not None: - aggregated_metrics[k].append(v) - if aggregated_metrics is not None: - aggregated_metrics = dict(aggregated_metrics) - return aggregated_metrics - - def _get_reasoning_agent(self, reasoning_model: Model) -> Optional[Agent]: - return Agent( - model=reasoning_model, - monitoring=self.monitoring, - telemetry=self.telemetry, - debug_mode=self.debug_mode, - ) - - def _format_reasoning_step_content( - self, run_response: TeamRunResponse, reasoning_step: ReasoningStep - ) -> str: - """Format content for a reasoning step without changing any existing logic.""" - step_content = "" - if reasoning_step.title: - step_content += f"## {reasoning_step.title}\n" - if reasoning_step.reasoning: - step_content += f"{reasoning_step.reasoning}\n" - if reasoning_step.action: - step_content += f"Action: {reasoning_step.action}\n" - if reasoning_step.result: - step_content += f"Result: {reasoning_step.result}\n" - step_content += "\n" - - # Get the current reasoning_content and append this step - current_reasoning_content = "" - if ( - hasattr(run_response, "reasoning_content") - and run_response.reasoning_content - ): - current_reasoning_content = run_response.reasoning_content - - # Create updated reasoning_content - updated_reasoning_content = current_reasoning_content + step_content - - return updated_reasoning_content - - def _reason( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - ) -> Iterator[TeamRunResponseEvent]: - if self.stream_intermediate_steps: - yield create_team_reasoning_started_event(from_run_response=run_response) - - use_default_reasoning = False - - # Get the reasoning model - reasoning_model: Optional[Model] = self.reasoning_model - reasoning_model_provided = reasoning_model is not None - if reasoning_model is None and self.model is not None: - from copy import deepcopy - - reasoning_model = deepcopy(self.model) - if reasoning_model is None: - log_warning( - "Reasoning error. Reasoning model is None, continuing regular session..." - ) - return - - # If a reasoning model is provided, use it to generate reasoning - if reasoning_model_provided: - from agno.reasoning.azure_ai_foundry import is_ai_foundry_reasoning_model - from agno.reasoning.deepseek import is_deepseek_reasoning_model - from agno.reasoning.groq import is_groq_reasoning_model - from agno.reasoning.helpers import get_reasoning_agent - from agno.reasoning.ollama import is_ollama_reasoning_model - from agno.reasoning.openai import is_openai_reasoning_model - - reasoning_agent = self.reasoning_agent or get_reasoning_agent( - reasoning_model=reasoning_model, monitoring=self.monitoring - ) - is_deepseek = is_deepseek_reasoning_model(reasoning_model) - is_groq = is_groq_reasoning_model(reasoning_model) - is_openai = is_openai_reasoning_model(reasoning_model) - is_ollama = is_ollama_reasoning_model(reasoning_model) - is_ai_foundry = is_ai_foundry_reasoning_model(reasoning_model) - - if is_deepseek or is_groq or is_openai or is_ollama or is_ai_foundry: - reasoning_message: Optional[Message] = None - if is_deepseek: - from agno.reasoning.deepseek import get_deepseek_reasoning - - log_debug("Starting DeepSeek Reasoning", center=True, symbol="=") - reasoning_message = get_deepseek_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_groq: - from agno.reasoning.groq import get_groq_reasoning - - log_debug("Starting Groq Reasoning", center=True, symbol="=") - reasoning_message = get_groq_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_openai: - from agno.reasoning.openai import get_openai_reasoning - - log_debug("Starting OpenAI Reasoning", center=True, symbol="=") - reasoning_message = get_openai_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_ollama: - from agno.reasoning.ollama import get_ollama_reasoning - - log_debug("Starting Ollama Reasoning", center=True, symbol="=") - reasoning_message = get_ollama_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_ai_foundry: - from agno.reasoning.azure_ai_foundry import get_ai_foundry_reasoning - - log_debug( - "Starting Azure AI Foundry Reasoning", center=True, symbol="=" - ) - reasoning_message = get_ai_foundry_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - - if reasoning_message is None: - log_warning( - "Reasoning error. Reasoning response is None, continuing regular session..." - ) - return - - run_messages.messages.append(reasoning_message) - # Add reasoning step to the Agent's run_response - update_run_response_with_reasoning( - run_response=run_response, - reasoning_steps=[ReasoningStep(result=reasoning_message.content)], - reasoning_agent_messages=[reasoning_message], - ) - if self.stream_intermediate_steps: - yield create_team_reasoning_completed_event( - from_run_response=run_response, - content=ReasoningSteps( - reasoning_steps=[ - ReasoningStep(result=reasoning_message.content) - ] - ), - content_type=ReasoningSteps.__name__, - ) - else: - log_warning( - f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning" - ) - use_default_reasoning = True - # If no reasoning model is provided, use default reasoning - else: - use_default_reasoning = True - - if use_default_reasoning: - from agno.reasoning.default import get_default_reasoning_agent - from agno.reasoning.helpers import ( - get_next_action, - update_messages_with_reasoning, - ) - - # Get default reasoning agent - use_json_mode: bool = self.use_json_mode - - reasoning_agent: Optional[Agent] = self.reasoning_agent # type: ignore - if reasoning_agent is None: - reasoning_agent = get_default_reasoning_agent( - reasoning_model=reasoning_model, - min_steps=self.reasoning_min_steps, - max_steps=self.reasoning_max_steps, - monitoring=self.monitoring, - telemetry=self.telemetry, - debug_mode=self.debug_mode, - use_json_mode=use_json_mode, - ) - - # Validate reasoning agent - if reasoning_agent is None: - log_warning( - "Reasoning error. Reasoning agent is None, continuing regular session..." - ) - return - # Ensure the reasoning agent response model is ReasoningSteps - if ( - reasoning_agent.response_model is not None - and not isinstance(reasoning_agent.response_model, type) - and not issubclass(reasoning_agent.response_model, ReasoningSteps) - ): - log_warning( - "Reasoning agent response model should be `ReasoningSteps`, continuing regular session..." - ) - return - # Ensure the reasoning model and agent do not show tool calls - reasoning_agent.show_tool_calls = False - - step_count = 1 - next_action = NextAction.CONTINUE - reasoning_messages: List[Message] = [] - all_reasoning_steps: List[ReasoningStep] = [] - log_debug("Starting Reasoning", center=True, symbol="=") - while ( - next_action == NextAction.CONTINUE - and step_count < self.reasoning_max_steps - ): - log_debug(f"Step {step_count}", center=True, symbol="-") - step_count += 1 - try: - # Run the reasoning agent - reasoning_agent_response: RunResponse = reasoning_agent.run( # type: ignore - messages=run_messages.get_input_messages() - ) - if ( - reasoning_agent_response.content is None - or reasoning_agent_response.messages is None - ): - log_warning( - "Reasoning error. Reasoning response is empty, continuing regular session..." - ) - break - - if reasoning_agent_response.content.reasoning_steps is None: - log_warning( - "Reasoning error. Reasoning steps are empty, continuing regular session..." - ) - break - - reasoning_steps: List[ReasoningStep] = ( - reasoning_agent_response.content.reasoning_steps - ) - all_reasoning_steps.extend(reasoning_steps) - # Yield reasoning steps - if self.stream_intermediate_steps: - for reasoning_step in reasoning_steps: - updated_reasoning_content = ( - self._format_reasoning_step_content( - run_response, reasoning_step - ) - ) - - yield create_team_reasoning_step_event( - from_run_response=run_response, - reasoning_step=reasoning_step, - reasoning_content=updated_reasoning_content, - ) - - # Find the index of the first assistant message - first_assistant_index = next( - ( - i - for i, m in enumerate(reasoning_agent_response.messages) - if m.role == "assistant" - ), - len(reasoning_agent_response.messages), - ) - # Extract reasoning messages starting from the message after the first assistant message - reasoning_messages = reasoning_agent_response.messages[ - first_assistant_index: - ] - - # Add reasoning step to the Agent's run_response - update_run_response_with_reasoning( - run_response=run_response, - reasoning_steps=reasoning_steps, - reasoning_agent_messages=reasoning_agent_response.messages, - ) - - # Get the next action - next_action = get_next_action(reasoning_steps[-1]) - if next_action == NextAction.FINAL_ANSWER: - break - except Exception as e: - log_error(f"Reasoning error: {e}") - break - - log_debug(f"Total Reasoning steps: {len(all_reasoning_steps)}") - log_debug("Reasoning finished", center=True, symbol="=") - - # Update the messages_for_model to include reasoning messages - update_messages_with_reasoning( - run_messages=run_messages, - reasoning_messages=reasoning_messages, - ) - - # Yield the final reasoning completed event - if self.stream_intermediate_steps: - yield create_team_reasoning_completed_event( - from_run_response=run_response, - content=ReasoningSteps(reasoning_steps=all_reasoning_steps), - content_type=ReasoningSteps.__name__, - ) - - async def _areason( - self, - run_response: TeamRunResponse, - run_messages: RunMessages, - ) -> AsyncIterator[TeamRunResponseEvent]: - if self.stream_intermediate_steps: - yield create_team_reasoning_started_event(from_run_response=run_response) - - use_default_reasoning = False - - # Get the reasoning model - reasoning_model: Optional[Model] = self.reasoning_model - reasoning_model_provided = reasoning_model is not None - if reasoning_model is None and self.model is not None: - from copy import deepcopy - - reasoning_model = deepcopy(self.model) - if reasoning_model is None: - log_warning( - "Reasoning error. Reasoning model is None, continuing regular session..." - ) - return - - # If a reasoning model is provided, use it to generate reasoning - if reasoning_model_provided: - from agno.reasoning.azure_ai_foundry import is_ai_foundry_reasoning_model - from agno.reasoning.deepseek import is_deepseek_reasoning_model - from agno.reasoning.groq import is_groq_reasoning_model - from agno.reasoning.helpers import get_reasoning_agent - from agno.reasoning.ollama import is_ollama_reasoning_model - from agno.reasoning.openai import is_openai_reasoning_model - - reasoning_agent = self.reasoning_agent or get_reasoning_agent( - reasoning_model=reasoning_model, monitoring=self.monitoring - ) - is_deepseek = is_deepseek_reasoning_model(reasoning_model) - is_groq = is_groq_reasoning_model(reasoning_model) - is_openai = is_openai_reasoning_model(reasoning_model) - is_ollama = is_ollama_reasoning_model(reasoning_model) - is_ai_foundry = is_ai_foundry_reasoning_model(reasoning_model) - - if is_deepseek or is_groq or is_openai or is_ollama or is_ai_foundry: - reasoning_message: Optional[Message] = None - if is_deepseek: - from agno.reasoning.deepseek import aget_deepseek_reasoning - - log_debug("Starting DeepSeek Reasoning", center=True, symbol="=") - reasoning_message = await aget_deepseek_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_groq: - from agno.reasoning.groq import aget_groq_reasoning - - log_debug("Starting Groq Reasoning", center=True, symbol="=") - reasoning_message = await aget_groq_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_openai: - from agno.reasoning.openai import aget_openai_reasoning - - log_debug("Starting OpenAI Reasoning", center=True, symbol="=") - reasoning_message = await aget_openai_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_ollama: - from agno.reasoning.ollama import get_ollama_reasoning - - log_debug("Starting Ollama Reasoning", center=True, symbol="=") - reasoning_message = get_ollama_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - elif is_ai_foundry: - from agno.reasoning.azure_ai_foundry import get_ai_foundry_reasoning - - log_debug( - "Starting Azure AI Foundry Reasoning", center=True, symbol="=" - ) - reasoning_message = get_ai_foundry_reasoning( - reasoning_agent=reasoning_agent, - messages=run_messages.get_input_messages(), - ) - - if reasoning_message is None: - log_warning( - "Reasoning error. Reasoning response is None, continuing regular session..." - ) - return - run_messages.messages.append(reasoning_message) - # Add reasoning step to the Agent's run_response - update_run_response_with_reasoning( - run_response=run_response, - reasoning_steps=[ReasoningStep(result=reasoning_message.content)], - reasoning_agent_messages=[reasoning_message], - ) - if self.stream_intermediate_steps: - yield create_team_reasoning_completed_event( - from_run_response=run_response, - content=ReasoningSteps( - reasoning_steps=[ - ReasoningStep(result=reasoning_message.content) - ] - ), - content_type=ReasoningSteps.__name__, - ) - else: - log_warning( - f"Reasoning model: {reasoning_model.__class__.__name__} is not a native reasoning model, defaulting to manual Chain-of-Thought reasoning" - ) - use_default_reasoning = True - # If no reasoning model is provided, use default reasoning - else: - use_default_reasoning = True - - if use_default_reasoning: - from agno.reasoning.default import get_default_reasoning_agent - from agno.reasoning.helpers import ( - get_next_action, - update_messages_with_reasoning, - ) - - # Get default reasoning agent - use_json_mode: bool = self.use_json_mode - reasoning_agent: Optional[Agent] = self.reasoning_agent # type: ignore - if reasoning_agent is None: - reasoning_agent = get_default_reasoning_agent( # type: ignore - reasoning_model=reasoning_model, - min_steps=self.reasoning_min_steps, - max_steps=self.reasoning_max_steps, - monitoring=self.monitoring, - telemetry=self.telemetry, - debug_mode=self.debug_mode, - use_json_mode=use_json_mode, - ) - - # Validate reasoning agent - if reasoning_agent is None: - log_warning( - "Reasoning error. Reasoning agent is None, continuing regular session..." - ) - return - # Ensure the reasoning agent response model is ReasoningSteps - if ( - reasoning_agent.response_model is not None - and not isinstance(reasoning_agent.response_model, type) - and not issubclass(reasoning_agent.response_model, ReasoningSteps) - ): - log_warning( - "Reasoning agent response model should be `ReasoningSteps`, continuing regular session..." - ) - return - - # Ensure the reasoning model and agent do not show tool calls - reasoning_agent.show_tool_calls = False - - step_count = 1 - next_action = NextAction.CONTINUE - reasoning_messages: List[Message] = [] - all_reasoning_steps: List[ReasoningStep] = [] - log_debug("Starting Reasoning", center=True, symbol="=") - while ( - next_action == NextAction.CONTINUE - and step_count < self.reasoning_max_steps - ): - log_debug(f"Step {step_count}", center=True, symbol="-") - step_count += 1 - try: - # Run the reasoning agent - reasoning_agent_response: RunResponse = await reasoning_agent.arun( # type: ignore - messages=run_messages.get_input_messages() - ) - if ( - reasoning_agent_response.content is None - or reasoning_agent_response.messages is None - ): - log_warning( - "Reasoning error. Reasoning response is empty, continuing regular session..." - ) - break - - if reasoning_agent_response.content.reasoning_steps is None: - log_warning( - "Reasoning error. Reasoning steps are empty, continuing regular session..." - ) - break - - reasoning_steps: List[ReasoningStep] = ( - reasoning_agent_response.content.reasoning_steps - ) - all_reasoning_steps.extend(reasoning_steps) - # Yield reasoning steps - if self.stream_intermediate_steps: - for reasoning_step in reasoning_steps: - updated_reasoning_content = ( - self._format_reasoning_step_content( - run_response, reasoning_step - ) - ) - - yield create_team_reasoning_step_event( - from_run_response=run_response, - reasoning_step=reasoning_step, - reasoning_content=updated_reasoning_content, - ) - - # Find the index of the first assistant message - first_assistant_index = next( - ( - i - for i, m in enumerate(reasoning_agent_response.messages) - if m.role == "assistant" - ), - len(reasoning_agent_response.messages), - ) - # Extract reasoning messages starting from the message after the first assistant message - reasoning_messages = reasoning_agent_response.messages[ - first_assistant_index: - ] - - # Add reasoning step to the Agent's run_response - update_run_response_with_reasoning( - run_response=run_response, - reasoning_steps=reasoning_steps, - reasoning_agent_messages=reasoning_agent_response.messages, - ) - - # Get the next action - next_action = get_next_action(reasoning_steps[-1]) - if next_action == NextAction.FINAL_ANSWER: - break - except Exception as e: - log_error(f"Reasoning error: {e}") - break - - log_debug(f"Total Reasoning steps: {len(all_reasoning_steps)}") - log_debug("Reasoning finished", center=True, symbol="=") - - # Update the messages_for_model to include reasoning messages - update_messages_with_reasoning( - run_messages=run_messages, - reasoning_messages=reasoning_messages, - ) - - # Yield the final reasoning completed event - if self.stream_intermediate_steps: - yield create_team_reasoning_completed_event( - from_run_response=run_response, - content=ReasoningSteps(reasoning_steps=all_reasoning_steps), - content_type=ReasoningSteps.__name__, - ) - - def _generator_wrapper( - self, event: TeamRunResponseEvent - ) -> Iterator[TeamRunResponseEvent]: - yield event - - async def _async_generator_wrapper( - self, event: TeamRunResponseEvent - ) -> AsyncIterator[TeamRunResponseEvent]: - yield event - - def _create_run_response( - self, - session_id: str, - content: Optional[Any] = None, - content_type: Optional[str] = None, - thinking: Optional[str] = None, - run_state: RunStatus = RunStatus.running, - tools: Optional[List[ToolExecution]] = None, - reasoning_content: Optional[str] = None, - audio: Optional[List[AudioArtifact]] = None, - images: Optional[List[ImageArtifact]] = None, - videos: Optional[List[VideoArtifact]] = None, - response_audio: Optional[AudioResponse] = None, - citations: Optional[Citations] = None, - model: Optional[str] = None, - messages: Optional[List[Message]] = None, - created_at: Optional[int] = None, - from_run_response: Optional[TeamRunResponse] = None, - ) -> TeamRunResponse: - extra_data = None - member_responses = None - formatted_tool_calls = None - if from_run_response: - content = from_run_response.content - content_type = from_run_response.content_type - audio = from_run_response.audio - images = from_run_response.images - videos = from_run_response.videos - response_audio = from_run_response.response_audio - model = from_run_response.model - messages = from_run_response.messages - extra_data = from_run_response.extra_data - member_responses = from_run_response.member_responses - citations = from_run_response.citations - tools = from_run_response.tools - formatted_tool_calls = from_run_response.formatted_tool_calls - reasoning_content = from_run_response.reasoning_content - - rr = TeamRunResponse( - run_id=self.run_id, - session_id=session_id, - team_id=self.team_id, - content=content, - thinking=thinking, - tools=tools, - audio=audio, - images=images, - videos=videos, - response_audio=response_audio, - reasoning_content=reasoning_content, - citations=citations, - model=model, - messages=messages, - extra_data=extra_data, - status=run_state, - ) - if formatted_tool_calls: - rr.formatted_tool_calls = formatted_tool_calls - if member_responses: - rr.member_responses = member_responses - if content_type is not None: - rr.content_type = content_type - if created_at is not None: - rr.created_at = created_at - return rr - - def _resolve_run_context(self) -> None: - from inspect import signature - - log_debug("Resolving context") - if self.context is not None: - if isinstance(self.context, dict): - for ctx_key, ctx_value in self.context.items(): - if callable(ctx_value): - try: - sig = signature(ctx_value) - if "agent" in sig.parameters: - resolved_ctx_value = ctx_value(agent=self) - else: - resolved_ctx_value = ctx_value() - if resolved_ctx_value is not None: - self.context[ctx_key] = resolved_ctx_value - except Exception as e: - log_warning(f"Failed to resolve context for {ctx_key}: {e}") - else: - self.context[ctx_key] = ctx_value - else: - log_warning("Context is not a dict") - - def determine_tools_for_model( - self, - model: Model, - session_id: str, - user_id: Optional[str] = None, - async_mode: bool = False, - knowledge_filters: Optional[Dict[str, Any]] = None, - message: Optional[Union[str, List, Dict, Message]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - audio: Optional[Sequence[Audio]] = None, - files: Optional[Sequence[File]] = None, - ) -> None: - # Prepare tools - _tools: List[Union[Toolkit, Callable, Function, Dict]] = [] - - # Add provided tools - if self.tools is not None: - for tool in self.tools: - _tools.append(tool) - - if self.read_team_history: - _tools.append(self.get_team_history_function(session_id=session_id)) - - if isinstance(self.memory, Memory) and self.enable_agentic_memory: - _tools.append( - self.get_update_user_memory_function( - user_id=user_id, async_mode=async_mode - ) - ) - - if self.enable_agentic_context: - _tools.append(self.get_set_shared_context_function(session_id=session_id)) - - if self.knowledge is not None or self.retriever is not None: - # Check if retriever is an async function but used in sync mode - from inspect import iscoroutinefunction - - if self.retriever is not None and iscoroutinefunction(self.retriever): - log_warning( - "Async retriever function is being used with synchronous agent.run() or agent.print_response(). " - "It is recommended to use agent.arun() or agent.aprint_response() instead." - ) - - if self.search_knowledge: - # Use async or sync search based on async_mode - if self.enable_agentic_knowledge_filters: - _tools.append( - self.search_knowledge_base_with_agentic_filters_function( - knowledge_filters=knowledge_filters, async_mode=async_mode - ) - ) - else: - _tools.append( - self.search_knowledge_base_function( - knowledge_filters=knowledge_filters, async_mode=async_mode - ) - ) - - if self.mode == "route": - user_message = self._get_user_message( - message, - audio=audio, - images=images, - videos=videos, - files=files, - user_id=user_id, - ) - forward_task_func: Function = self.get_forward_task_function( - message=user_message, - session_id=session_id, - stream=self.stream or False, - stream_intermediate_steps=self.stream_intermediate_steps, - async_mode=False, - images=images, # type: ignore - videos=videos, # type: ignore - audio=audio, # type: ignore - files=files, # type: ignore - knowledge_filters=knowledge_filters, - ) - _tools.append(forward_task_func) - if self.get_member_information_tool: - _tools.append(self.get_member_information) - - elif self.mode == "coordinate": - _tools.append( - self.get_transfer_task_function( - session_id=session_id, - stream=self.stream or False, - stream_intermediate_steps=self.stream_intermediate_steps, - async_mode=False, - images=images, # type: ignore - videos=videos, # type: ignore - audio=audio, # type: ignore - files=files, # type: ignore - knowledge_filters=knowledge_filters, - ) - ) - if self.get_member_information_tool: - _tools.append(self.get_member_information) - - elif self.mode == "collaborate": - run_member_agents_func = self.get_run_member_agents_function( - session_id=session_id, - stream=self.stream or False, - stream_intermediate_steps=self.stream_intermediate_steps, - async_mode=False, - images=images, # type: ignore - videos=videos, # type: ignore - audio=audio, # type: ignore - files=files, # type: ignore - ) - _tools.append(run_member_agents_func) - - if self.get_member_information_tool: - _tools.append(self.get_member_information) - - self._functions_for_model = {} - self._tools_for_model = [] - - # Get Agent tools - if len(_tools) > 0: - log_debug("Processing tools for model") - - # Check if we need strict mode for the model - strict = False - if ( - self.response_model is not None - and not self.use_json_mode - and model.supports_native_structured_outputs - ): - strict = True - - for tool in _tools: - if isinstance(tool, Dict): - # If a dict is passed, it is a builtin tool - # that is run by the model provider and not the Agent - self._tools_for_model.append(tool) - log_debug(f"Included builtin tool {tool}") - - elif isinstance(tool, Toolkit): - # For each function in the toolkit and process entrypoint - for name, func in tool.functions.items(): - # If the function does not exist in self.functions - if name not in self._functions_for_model: - func._agent = self - func._team = self - func.process_entrypoint(strict=strict) - if strict: - func.strict = True - if self.tool_hooks: - func.tool_hooks = self.tool_hooks - self._functions_for_model[name] = func - self._tools_for_model.append( - {"type": "function", "function": func.to_dict()} - ) - log_debug(f"Added tool {name} from {tool.name}") - - # Add instructions from the toolkit - if tool.add_instructions and tool.instructions is not None: - if self._tool_instructions is None: - self._tool_instructions = [] - self._tool_instructions.append(tool.instructions) - - elif isinstance(tool, Function): - if tool.name not in self._functions_for_model: - tool._agent = self - tool._team = self - tool.process_entrypoint(strict=strict) - if strict and tool.strict is None: - tool.strict = True - if self.tool_hooks: - tool.tool_hooks = self.tool_hooks - self._functions_for_model[tool.name] = tool - self._tools_for_model.append( - {"type": "function", "function": tool.to_dict()} - ) - log_debug(f"Added tool {tool.name}") - - # Add instructions from the Function - if tool.add_instructions and tool.instructions is not None: - if self._tool_instructions is None: - self._tool_instructions = [] - self._tool_instructions.append(tool.instructions) - - elif callable(tool): - # We add the tools, which are callable functions - try: - func = Function.from_callable(tool, strict=strict) - func._agent = self - func._team = self - if strict: - func.strict = True - if self.tool_hooks: - func.tool_hooks = self.tool_hooks - self._functions_for_model[func.name] = func - self._tools_for_model.append( - {"type": "function", "function": func.to_dict()} - ) - log_debug(f"Added tool {func.name}") - except Exception as e: - log_warning(f"Could not add tool {tool}: {e}") - - def get_members_system_message_content(self, indent: int = 0) -> str: - system_message_content = "" - for idx, member in enumerate(self.members): - url_safe_member_id = self._get_member_id(member) - - if isinstance(member, Team): - system_message_content += f"{indent * ' '} - Team: {member.name}\n" - system_message_content += f"{indent * ' '} - ID: {url_safe_member_id}\n" - if member.members is not None: - system_message_content += member.get_members_system_message_content( - indent=indent + 2 - ) - else: - system_message_content += f"{indent * ' '} - Agent {idx + 1}:\n" - if member.name is not None: - system_message_content += ( - f"{indent * ' '} - ID: {url_safe_member_id}\n" - ) - system_message_content += ( - f"{indent * ' '} - Name: {member.name}\n" - ) - if member.role is not None: - system_message_content += ( - f"{indent * ' '} - Role: {member.role}\n" - ) - if member.tools is not None and self.add_member_tools_to_system_message: - system_message_content += f"{indent * ' '} - Member tools:\n" - for _tool in member.tools: - if isinstance(_tool, Toolkit): - for _func in _tool.functions.values(): - if _func.entrypoint: - system_message_content += ( - f"{indent * ' '} - {_func.name}\n" - ) - elif isinstance(_tool, Function) and _tool.entrypoint: - system_message_content += ( - f"{indent * ' '} - {_tool.name}\n" - ) - elif callable(_tool): - system_message_content += ( - f"{indent * ' '} - {_tool.__name__}\n" - ) - - return system_message_content - - def get_system_message( - self, - session_id: str, - user_id: Optional[str] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - ) -> Optional[Message]: - """Get the system message for the team.""" - - # 1. If the system_message is provided, use that. - if self.system_message is not None: - if isinstance(self.system_message, Message): - return self.system_message - - sys_message_content: str = "" - if isinstance(self.system_message, str): - sys_message_content = self.system_message - elif callable(self.system_message): - sys_message_content = self.system_message(agent=self) - if not isinstance(sys_message_content, str): - raise Exception("system_message must return a string") - - # Format the system message with the session state variables - if self.add_state_in_messages: - sys_message_content = self._format_message_with_state_variables( - sys_message_content, user_id=user_id - ) - - # type: ignore - return Message(role=self.system_message_role, content=sys_message_content) - - # 1. Build and return the default system message for the Team. - # 1.1 Build the list of instructions for the system message - self.model = cast(Model, self.model) - instructions: List[str] = [] - if self.instructions is not None: - _instructions = self.instructions - if callable(self.instructions): - _instructions = self.instructions(agent=self) - - if isinstance(_instructions, str): - instructions.append(_instructions) - elif isinstance(_instructions, list): - instructions.extend(_instructions) - - # 1.2 Add instructions from the Model - _model_instructions = self.model.get_instructions_for_model( - self._tools_for_model - ) - if _model_instructions is not None: - instructions.extend(_model_instructions) - - # 1.3 Build a list of additional information for the system message - additional_information: List[str] = [] - # 1.3.1 Add instructions for using markdown - if self.markdown and self.response_model is None: - additional_information.append("Use markdown to format your answers.") - # 1.3.2 Add the current datetime - if self.add_datetime_to_instructions: - from datetime import datetime - - additional_information.append(f"The current time is {datetime.now()}") - - # 1.3.3 Add the current location - if self.add_location_to_instructions: - from agno.utils.location import get_location - - location = get_location() - if location: - location_str = ", ".join( - filter( - None, - [ - location.get("city"), - location.get("region"), - location.get("country"), - ], - ) - ) - if location_str: - additional_information.append( - f"Your approximate location is: {location_str}." - ) - - if self.knowledge is not None and self.enable_agentic_knowledge_filters: - valid_filters = getattr(self.knowledge, "valid_metadata_filters", None) - if valid_filters: - valid_filters_str = ", ".join(valid_filters) - additional_information.append( - dedent(f""" - The knowledge base contains documents with these metadata filters: {valid_filters_str}. - Always use filters when the user query indicates specific metadata. - Examples: - 1. If the user asks about a specific person like "Jordan Mitchell", you MUST use the search_knowledge_base tool with the filters parameter set to {{'': ''}}. - 2. If the user asks about a specific document type like "contracts", you MUST use the search_knowledge_base tool with the filters parameter set to {{'document_type': 'contract'}}. - 4. If the user asks about a specific location like "documents from New York", you MUST use the search_knowledge_base tool with the filters parameter set to {{'': 'New York'}}. - General Guidelines: - - Always analyze the user query to identify relevant metadata. - - Use the most specific filter(s) possible to narrow down results. - - If multiple filters are relevant, combine them in the filters parameter (e.g., {{'name': 'Jordan Mitchell', 'document_type': 'contract'}}). - - Ensure the filter keys match the valid metadata filters: {valid_filters_str}. - You can use the search_knowledge_base tool to search the knowledge base and get the most relevant documents. Make sure to pass the filters as [Dict[str: Any]] to the tool. FOLLOW THIS STRUCTURE STRICTLY. - """) - ) - - # 2 Build the default system message for the Agent. - system_message_content: str = "" - system_message_content += ( - "You are the leader of a team and sub-teams of AI Agents.\n" - ) - system_message_content += ( - "Your task is to coordinate the team to complete the user's request.\n" - ) - - system_message_content += "\nHere are the members in your team:\n" - system_message_content += "\n" - system_message_content += self.get_members_system_message_content() - if self.get_member_information_tool: - system_message_content += "If you need to get information about your team members, you can use the `get_member_information` tool at any time.\n" - system_message_content += "\n" - - system_message_content += "\n\n" - if self.mode == "coordinate": - system_message_content += ( - "- You can either respond directly or transfer tasks to members in your team with the highest likelihood of completing the user's request.\n" - "- Carefully analyze the tools available to the members and their roles before transferring tasks.\n" - "- You cannot use a member tool directly. You can only transfer tasks to members.\n" - "- When you transfer a task to another member, make sure to include:\n" - " - member_id (str): The ID of the member to forward the task to.\n" - " - task_description (str): A clear description of the task.\n" - " - expected_output (str): The expected output.\n" - "- You can transfer tasks to multiple members at once.\n" - "- You must always analyze the responses from members before responding to the user.\n" - "- After analyzing the responses from the members, if you feel the task has been completed, you can stop and respond to the user.\n" - "- If you are not satisfied with the responses from the members, you should re-assign the task.\n" - ) - elif self.mode == "route": - system_message_content += ( - "- You can either respond directly or forward tasks to members in your team with the highest likelihood of completing the user's request.\n" - "- Carefully analyze the tools available to the members and their roles before forwarding tasks.\n" - "- When you forward a task to another Agent, make sure to include:\n" - " - member_id (str): The ID of the member to forward the task to.\n" - " - expected_output (str): The expected output.\n" - "- You can forward tasks to multiple members at once.\n" - ) - elif self.mode == "collaborate": - system_message_content += ( - "- You can either respond directly or use the `run_member_agents` tool to run all members in your team to get a collaborative response.\n" - "- To run the members in your team, call `run_member_agents` ONLY once. This will run all members in your team.\n" - "- Analyze the responses from all members and evaluate whether the task has been completed.\n" - "- If you feel the task has been completed, you can stop and respond to the user.\n" - ) - system_message_content += "\n\n" - - if self.enable_agentic_context: - system_message_content += "\n" - system_message_content += "You have access to a shared context that will be shared with all members of the team.\n" - system_message_content += "Use this shared context to improve inter-agent communication and coordination.\n" - system_message_content += "It is important that you update the shared context as often as possible.\n" - system_message_content += ( - "To update the shared context, use the `set_shared_context` tool.\n" - ) - system_message_content += "\n\n" - - if self.name is not None: - system_message_content += f"Your name is: {self.name}\n\n" - - if self.success_criteria: - system_message_content += ( - "Your task is successful when the following criteria is met:\n" - ) - system_message_content += "\n" - system_message_content += f"{self.success_criteria}\n" - system_message_content += "\n" - system_message_content += ( - "Stop the team run when the success_criteria is met.\n\n" - ) - - # Attached media - if ( - audio is not None - or images is not None - or videos is not None - or files is not None - ): - system_message_content += "\n" - system_message_content += ( - "You have the following media attached to your message:\n" - ) - if audio is not None and len(audio) > 0: - system_message_content += " - Audio\n" - if images is not None and len(images) > 0: - system_message_content += " - Images\n" - if videos is not None and len(videos) > 0: - system_message_content += " - Videos\n" - if files is not None and len(files) > 0: - system_message_content += " - Files\n" - system_message_content += "\n\n" - - # Then add memories to the system prompt - if self.memory: - if isinstance(self.memory, Memory) and self.add_memory_references: - if not user_id: - user_id = "default" - user_memories = self.memory.get_user_memories(user_id=user_id) # type: ignore - if user_memories and len(user_memories) > 0: - system_message_content += "You have access to memories from previous interactions with the user that you can use:\n\n" - system_message_content += "" - for _memory in user_memories: # type: ignore - system_message_content += f"\n- {_memory.memory}" - system_message_content += ( - "\n\n\n" - ) - system_message_content += ( - "Note: this information is from previous interactions and may be updated in this conversation. " - "You should always prefer information from this conversation over the past memories.\n\n" - ) - else: - system_message_content += ( - "You have the capability to retain memories from previous interactions with the user, " - "but have not had any interactions with the user yet.\n" - ) - - if self.enable_agentic_memory: - system_message_content += ( - "You have access to the `update_user_memory` tool.\n" - "You can use the `update_user_memory` tool to add new memories, update existing memories, delete memories, or clear all memories.\n" - "Memories should include details that could personalize ongoing interactions with the user.\n" - "Use this tool to add new memories or update existing memories that you identify in the conversation.\n" - "Use this tool if the user asks to update their memory, delete a memory, or clear all memories.\n" - "If you use the `update_user_memory` tool, remember to pass on the response to the user.\n\n" - ) - - # Then add a summary of the interaction to the system prompt - if isinstance(self.memory, Memory) and self.add_session_summary_references: - if not user_id: - user_id = "default" - session_summary: SessionSummary = self.memory.summaries.get( - user_id, {} - ).get(session_id, None) # type: ignore - if session_summary is not None: - system_message_content += ( - "Here is a brief summary of your previous interactions:\n\n" - ) - system_message_content += "\n" - system_message_content += session_summary.summary - system_message_content += ( - "\n\n\n" - ) - system_message_content += ( - "Note: this information is from previous interactions and may be outdated. " - "You should ALWAYS prefer information from this conversation over the past summary.\n\n" - ) - - if self.description is not None: - system_message_content += ( - f"\n{self.description}\n\n\n" - ) - - # 3.3.5 Then add instructions for the Agent - if len(instructions) > 0: - system_message_content += "" - if len(instructions) > 1: - for _upi in instructions: - system_message_content += f"\n- {_upi}" - else: - system_message_content += "\n" + instructions[0] - system_message_content += "\n\n\n" - # 3.3.6 Add additional information - if len(additional_information) > 0: - system_message_content += "" - for _ai in additional_information: - system_message_content += f"\n- {_ai}" - system_message_content += "\n\n\n" - # 3.3.7 Then add instructions for the tools - if self._tool_instructions is not None: - for _ti in self._tool_instructions: - system_message_content += f"{_ti}\n" - - # Format the system message with the session state variables - if self.add_state_in_messages: - system_message_content = self._format_message_with_state_variables( - system_message_content, user_id=user_id - ) - - system_message_from_model = self.model.get_system_message_for_model( - self._tools_for_model - ) - if system_message_from_model is not None: - system_message_content += system_message_from_model - - if self.expected_output is not None: - system_message_content += f"\n{self.expected_output.strip()}\n\n\n" - - if self.additional_context is not None: - system_message_content += f"\n{self.additional_context.strip()}\n\n\n" - - # Add the JSON output prompt if response_model is provided and structured_outputs is False - if ( - self.response_model is not None - and self.use_json_mode - and self.model - and self.model.supports_native_structured_outputs - ): - system_message_content += f"{self._get_json_output_prompt()}" - - return Message( - role=self.system_message_role, content=system_message_content.strip() - ) - - def get_run_messages( - self, - *, - session_id: str, - user_id: Optional[str] = None, - message: Optional[Union[str, List, Dict, Message]] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs: Any, - ) -> RunMessages: - """This function returns a RunMessages object with the following attributes: - - system_message: The system message for this run - - user_message: The user message for this run - - messages: List of messages to send to the model - - To build the RunMessages object: - 1. Add system message to run_messages - 2. Add history to run_messages - 3. Add user message to run_messages - - """ - # Initialize the RunMessages object - run_messages = RunMessages() - - # 1. Add system message to run_messages - system_message = self.get_system_message( - session_id=session_id, - user_id=user_id, - images=images, - audio=audio, - videos=videos, - files=files, - ) - if system_message is not None: - run_messages.system_message = system_message - run_messages.messages.append(system_message) - - # 2. Add history to run_messages - if self.enable_team_history or self.add_history_to_messages: - from copy import deepcopy - - history = [] - if isinstance(self.memory, TeamMemory): - history = self.memory.get_messages_from_last_n_runs( - last_n=self.num_history_runs, skip_role=self.system_message_role - ) - elif isinstance(self.memory, Memory): - history = self.memory.get_messages_from_last_n_runs( - session_id=session_id, - last_n=self.num_history_runs, - skip_role=self.system_message_role, - ) - - if len(history) > 0: - # Create a deep copy of the history messages to avoid modifying the original messages - history_copy = [deepcopy(msg) for msg in history] - - # Tag each message as coming from history - for _msg in history_copy: - _msg.from_history = True - - log_debug(f"Adding {len(history_copy)} messages from history") - - # Extend the messages with the history - run_messages.messages += history_copy - - # 3. Add user message to run_messages - user_message = self._get_user_message( - message, - user_id=user_id, - audio=audio, - images=images, - videos=videos, - files=files, - knowledge_filters=knowledge_filters, - **kwargs, - ) - - # Add user message to run_messages - if user_message is not None: - run_messages.user_message = user_message - run_messages.messages.append(user_message) - - return run_messages - - def _get_user_message( - self, - message: Optional[Union[str, List, Dict, Message]] = None, - user_id: Optional[str] = None, - audio: Optional[Sequence[Audio]] = None, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - **kwargs, - ): - # Get references from the knowledge base to use in the user message - references = None - self.run_response = cast(TeamRunResponse, self.run_response) - if self.add_references and message: - message_str: str - if isinstance(message, str): - message_str = message - elif callable(message): - message_str = message(agent=self) - else: - raise Exception( - "message must be a string or a callable when add_references is True" - ) - - try: - retrieval_timer = Timer() - retrieval_timer.start() - docs_from_knowledge = self.get_relevant_docs_from_knowledge( - query=message_str, filters=knowledge_filters, **kwargs - ) - if docs_from_knowledge is not None: - references = MessageReferences( - query=message_str, - references=docs_from_knowledge, - time=round(retrieval_timer.elapsed, 4), - ) - # Add the references to the run_response - if self.run_response.extra_data is None: - self.run_response.extra_data = RunResponseExtraData() - if self.run_response.extra_data.references is None: - self.run_response.extra_data.references = [] - self.run_response.extra_data.references.append(references) - retrieval_timer.stop() - log_debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s") - except Exception as e: - log_warning(f"Failed to get references: {e}") - - # Build user message if message is None, str or list - user_message_content: str = "" - if isinstance(message, str) or isinstance(message, list): - if self.add_state_in_messages: - if isinstance(message, str): - user_message_content = self._format_message_with_state_variables( - message, user_id=user_id - ) - elif isinstance(message, list): - user_message_content = "\n".join( - [ - self._format_message_with_state_variables( - msg, user_id=user_id - ) - for msg in message - ] - ) - else: - if isinstance(message, str): - user_message_content = message - else: - user_message_content = "\n".join(message) - - # Add references to user message - if ( - self.add_references - and references is not None - and references.references is not None - and len(references.references) > 0 - ): - user_message_content += "\n\nUse the following references from the knowledge base if it helps:\n" - user_message_content += "\n" - user_message_content += ( - self._convert_documents_to_string(references.references) + "\n" - ) - user_message_content += "" - # Add context to user message - if self.add_context and self.context is not None: - user_message_content += "\n\n\n" - user_message_content += ( - self._convert_context_to_string(self.context) + "\n" - ) - user_message_content += "" - - return Message( - role="user", - content=user_message_content, - audio=audio, - images=images, - videos=videos, - files=files, - **kwargs, - ) - - # Build the default user message for the Agent - elif message is None: - # If we have any media, return a message with empty content - if ( - images is not None - or audio is not None - or videos is not None - or files is not None - ): - return Message( - role="user", - content="", - images=images, - audio=audio, - videos=videos, - files=files, - **kwargs, - ) - else: - # If the message is None, return None - return None - - # If message is provided as a Message, use it directly - elif isinstance(message, Message): - return message - # If message is provided as a dict, try to validate it as a Message - elif isinstance(message, dict): - try: - return Message.model_validate(message) - except Exception as e: - log_warning(f"Failed to validate message: {e}") - - def _format_message_with_state_variables( - self, message: Any, user_id: Optional[str] = None - ) -> Any: - """Format a message with the session state variables.""" - import re - import string - - if not isinstance(message, str): - return message - - format_variables = ChainMap( - self.session_state or {}, - self.team_session_state or {}, - self.context or {}, - self.extra_data or {}, - {"user_id": user_id} if user_id is not None else {}, - ) - converted_msg = message - for var_name in format_variables.keys(): - # Only convert standalone {var_name} patterns, not nested ones - pattern = r"\{" + re.escape(var_name) + r"\}" - replacement = "${" + var_name + "}" - converted_msg = re.sub(pattern, replacement, converted_msg) - - # Use Template to safely substitute variables - template = string.Template(converted_msg) - try: - result = template.safe_substitute(format_variables) - return result - except Exception as e: - log_warning(f"Template substitution failed: {e}") - return message - - def _convert_context_to_string(self, context: Dict[str, Any]) -> str: - """Convert the context dictionary to a string representation. - - Args: - context: Dictionary containing context data - - Returns: - String representation of the context, or empty string if conversion fails - """ - - if context is None: - return "" - - import json - - try: - return json.dumps(context, indent=2, default=str) - except (TypeError, ValueError, OverflowError) as e: - log_warning(f"Failed to convert context to JSON: {e}") - # Attempt a fallback conversion for non-serializable objects - sanitized_context = {} - for key, value in context.items(): - try: - # Try to serialize each value individually - json.dumps({key: value}, default=str) - sanitized_context[key] = value - except Exception as e: - log_error(f"Failed to serialize to JSON: {e}") - # If serialization fails, convert to string representation - sanitized_context[key] = str(value) - - try: - return json.dumps(sanitized_context, indent=2) - except Exception as e: - log_error(f"Failed to convert sanitized context to JSON: {e}") - return str(context) - - def _get_json_output_prompt(self) -> str: - """Return the JSON output prompt for the Agent. - - This is added to the system prompt when the response_model is set and structured_outputs is False. - """ - import json - - json_output_prompt = ( - "Provide your output as a JSON containing the following fields:" - ) - if self.response_model is not None: - if isinstance(self.response_model, str): - json_output_prompt += "\n" - json_output_prompt += f"\n{self.response_model}" - json_output_prompt += "\n" - elif isinstance(self.response_model, list): - json_output_prompt += "\n" - json_output_prompt += f"\n{json.dumps(self.response_model)}" - json_output_prompt += "\n" - elif issubclass(self.response_model, BaseModel): - json_schema = self.response_model.model_json_schema() - if json_schema is not None: - response_model_properties = {} - json_schema_properties = json_schema.get("properties") - if json_schema_properties is not None: - for ( - field_name, - field_properties, - ) in json_schema_properties.items(): - formatted_field_properties = { - prop_name: prop_value - for prop_name, prop_value in field_properties.items() - if prop_name != "title" - } - response_model_properties[field_name] = ( - formatted_field_properties - ) - json_schema_defs = json_schema.get("$defs") - if json_schema_defs is not None: - response_model_properties["$defs"] = {} - for def_name, def_properties in json_schema_defs.items(): - def_fields = def_properties.get("properties") - formatted_def_properties = {} - if def_fields is not None: - for field_name, field_properties in def_fields.items(): - formatted_field_properties = { - prop_name: prop_value - for prop_name, prop_value in field_properties.items() - if prop_name != "title" - } - formatted_def_properties[field_name] = ( - formatted_field_properties - ) - if len(formatted_def_properties) > 0: - response_model_properties["$defs"][def_name] = ( - formatted_def_properties - ) - - if len(response_model_properties) > 0: - json_output_prompt += "\n" - json_output_prompt += f"\n{json.dumps([key for key in response_model_properties.keys() if key != '$defs'])}" - json_output_prompt += "\n" - json_output_prompt += ( - "\n\nHere are the properties for each field:" - ) - json_output_prompt += "\n" - json_output_prompt += ( - f"\n{json.dumps(response_model_properties, indent=2)}" - ) - json_output_prompt += "\n" - else: - log_warning(f"Could not build json schema for {self.response_model}") - else: - json_output_prompt += "Provide the output as JSON." - - json_output_prompt += "\nStart your response with `{` and end it with `}`." - json_output_prompt += "\nYour output will be passed to json.loads() to convert it to a Python object." - json_output_prompt += "\nMake sure it only contains valid JSON." - return json_output_prompt - - def _update_team_media( - self, run_response: Union[TeamRunResponse, RunResponse] - ) -> None: - """Update the team state with the run response.""" - if run_response.images is not None: - if self.images is None: - self.images = [] - self.images.extend(run_response.images) - if run_response.videos is not None: - if self.videos is None: - self.videos = [] - self.videos.extend(run_response.videos) - if run_response.audio is not None: - if self.audio is None: - self.audio = [] - self.audio.extend(run_response.audio) - - ########################################################################### - # Built-in Tools - ########################################################################### - - def get_update_user_memory_function( - self, user_id: Optional[str] = None, async_mode: bool = False - ) -> Callable: - def update_user_memory(task: str) -> str: - """ - Use this function to submit a task to modify the Agent's memory. - Describe the task in detail and be specific. - The task can include adding a memory, updating a memory, deleting a memory, or clearing all memories. - - Args: - task: The task to update the memory. Be specific and describe the task in detail. - - Returns: - str: A string indicating the status of the update. - """ - self.memory = cast(Memory, self.memory) - response = self.memory.update_memory_task(task=task, user_id=user_id) - return response - - async def aupdate_user_memory(task: str) -> str: - """ - Use this function to submit a task to modify the Agent's memory. - Describe the task in detail and be specific. - The task can include adding a memory, updating a memory, deleting a memory, or clearing all memories. - - Args: - task: The task to update the memory. Be specific and describe the task in detail. - - Returns: - str: A string indicating the status of the update. - """ - self.memory = cast(Memory, self.memory) - response = await self.memory.aupdate_memory_task(task=task, user_id=user_id) - return response - - if async_mode: - return aupdate_user_memory - else: - return update_user_memory - - def get_member_information(self) -> str: - """Get information about the members of the team, including their IDs, names, and roles.""" - return self.get_members_system_message_content(indent=0) - - def get_team_history_function(self, session_id: str) -> Callable: - def get_team_history(num_chats: Optional[int] = None) -> str: - """ - Use this function to get the team chat history. - - Args: - num_chats: The number of chats to return. - Each chat contains 2 messages. One from the team and one from the user. - Default: None - - Returns: - str: A JSON string containing a list of dictionaries representing the team chat history. - - Example: - - To get the last chat, use num_chats=1 - - To get the last 5 chats, use num_chats=5 - - To get all chats, use num_chats=None - - To get the first chat, use num_chats=None and take the first message - """ - import json - - history: List[Dict[str, Any]] = [] - if isinstance(self.memory, TeamMemory): - team_chats = self.memory.get_all_messages() - - if len(team_chats) == 0: - return "" - - chats_added = 0 - for chat in team_chats[::-1]: - history.insert(0, chat[1].to_dict()) - history.insert(0, chat[0].to_dict()) - chats_added += 1 - if num_chats is not None and chats_added >= num_chats: - break - - elif isinstance(self.memory, Memory): - all_chats = self.memory.get_messages_for_session(session_id=session_id) - - if len(all_chats) == 0: - return "" - - for chat in all_chats[::-1]: # type: ignore - history.insert(0, chat.to_dict()) # type: ignore - - if num_chats is not None: - history = history[:num_chats] - - else: - return "" - - return json.dumps(history) - - return get_team_history - - def get_set_shared_context_function(self, session_id: str) -> Callable: - def set_shared_context(state: Union[str, dict]) -> str: - """ - Set or update the team's shared context with the given state. - - Args: - state (str or dict): The state to set as the team context. - """ - if isinstance(self.memory, TeamMemory): - if isinstance(state, str): - self.memory.set_team_context_text(state) # type: ignore - elif isinstance(state, dict): - self.memory.set_team_context_text(json.dumps(state)) # type: ignore - msg = f"Current team context: {self.memory.get_team_context_str()}" # type: ignore - else: - self.memory.set_team_context_text(session_id=session_id, text=state) # type: ignore - msg = f"Current team context: {self.memory.get_team_context_str(session_id=session_id)}" # type: ignore - log_debug(msg) # type: ignore - return msg - - return set_shared_context - - def _update_team_session_state(self, member_agent: Union[Agent, "Team"]) -> None: - """Update team session state from either an Agent or nested Team member""" - if member_agent.team_session_state is not None: - if self.team_session_state is None: - self.team_session_state = member_agent.team_session_state - else: - merge_dictionaries( - self.team_session_state, member_agent.team_session_state - ) - - def get_run_member_agents_function( - self, - session_id: str, - stream: bool = False, - stream_intermediate_steps: bool = False, - async_mode: bool = False, - images: Optional[List[Image]] = None, - videos: Optional[List[Video]] = None, - audio: Optional[List[Audio]] = None, - files: Optional[List[File]] = None, - ) -> Function: - if not images: - images = [] - if not videos: - videos = [] - if not audio: - audio = [] - if not files: - files = [] - - def _determine_team_context( - self, session_id: str - ) -> Tuple[Optional[str], Optional[str]]: - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - team_context_str = None - if self.enable_agentic_context: - team_context_str = self.memory.get_team_context_str() - - team_member_interactions_str = None - if self.share_member_interactions: - team_member_interactions_str = ( - self.memory.get_team_member_interactions_str() - ) - if context_images := self.memory.get_team_context_images(): - images.extend( - [Image.from_artifact(img) for img in context_images] - ) - if context_videos := self.memory.get_team_context_videos(): - videos.extend( - [Video.from_artifact(vid) for vid in context_videos] - ) - if context_audio := self.memory.get_team_context_audio(): - audio.extend( - [Audio.from_artifact(aud) for aud in context_audio] - ) - else: - self.memory = cast(Memory, self.memory) - team_context_str = None - if self.enable_agentic_context: - team_context_str = self.memory.get_team_context_str( - session_id=session_id - ) # type: ignore - - team_member_interactions_str = None - if self.share_member_interactions: - team_member_interactions_str = ( - self.memory.get_team_member_interactions_str( - session_id=session_id - ) - ) # type: ignore - if context_images := self.memory.get_team_context_images( - session_id=session_id - ): # type: ignore - images.extend( - [Image.from_artifact(img) for img in context_images] - ) - if context_videos := self.memory.get_team_context_videos( - session_id=session_id - ): # type: ignore - videos.extend( - [Video.from_artifact(vid) for vid in context_videos] - ) - if context_audio := self.memory.get_team_context_audio( - session_id=session_id - ): # type: ignore - audio.extend( - [Audio.from_artifact(aud) for aud in context_audio] - ) - return team_context_str, team_member_interactions_str - - def run_member_agents( - task_description: str, expected_output: Optional[str] = None - ) -> Iterator[Union[RunResponseEvent, TeamRunResponseEvent, str]]: - """ - Send the same task to all the member agents and return the responses. - - Args: - task_description (str): The task description to send to the member agents. - expected_output (str): The expected output from the member agents. - - Returns: - str: The responses from the member agents. - """ - # Make sure for the member agent, we are using the agent logger - use_agent_logger() - self.memory = cast(TeamMemory, self.memory) - - # 2. Determine team context to send - team_context_str, team_member_interactions_str = _determine_team_context( - self, session_id - ) - - # 3. Create the member agent task - member_agent_task = self._formate_member_agent_task( - task_description, - expected_output, - team_context_str, - team_member_interactions_str, - ) - - for member_agent_index, member_agent in enumerate(self.members): - self._initialize_member(member_agent, session_id=session_id) - - if stream: - member_agent_run_response_stream = member_agent.run( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - ) - for ( - member_agent_run_response_chunk - ) in member_agent_run_response_stream: - check_if_run_cancelled(member_agent_run_response_chunk) - yield member_agent_run_response_chunk - else: - member_agent_run_response = member_agent.run( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=False, - ) - - check_if_run_cancelled(member_agent_run_response) - - try: - if member_agent_run_response.content is None and ( - member_agent_run_response.tools is None - or len(member_agent_run_response.tools) == 0 - ): - yield f"Agent {member_agent.name}: No response from the member agent." - elif isinstance(member_agent_run_response.content, str): - if len(member_agent_run_response.content.strip()) > 0: - yield f"Agent {member_agent.name}: {member_agent_run_response.content}" - elif ( - member_agent_run_response.tools is not None - and len(member_agent_run_response.tools) > 0 - ): - yield f"Agent {member_agent.name}: {','.join([tool.result for tool in member_agent_run_response.tools])}" # type: ignore - elif issubclass( - type(member_agent_run_response.content), BaseModel - ): - yield f"Agent {member_agent.name}: {member_agent_run_response.content.model_dump_json(indent=2)}" # type: ignore - else: - import json - - yield f"Agent {member_agent.name}: {json.dumps(member_agent_run_response.content, indent=2)}" - except Exception as e: - yield f"Agent {member_agent.name}: Error - {str(e)}" - - # Update the memory - member_name = ( - member_agent.name - if member_agent.name - else f"agent_{member_agent_index}" - ) - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - self.memory.add_interaction_to_team_context( - member_name=member_name, - task=task_description, - run_response=member_agent.run_response, # type: ignore - ) - else: - self.memory = cast(Memory, self.memory) - self.memory.add_interaction_to_team_context( - session_id=session_id, - member_name=member_name, - task=task_description, - run_response=member_agent.run_response, # type: ignore - ) - - # Add the member run to the team run response - self.run_response = cast(TeamRunResponse, self.run_response) - self.run_response.add_member_run(member_agent.run_response) # type: ignore - - # Update team session state - self._update_team_session_state(member_agent) - - # Update the team media - self._update_team_media(member_agent.run_response) # type: ignore - - # Afterward, switch back to the team logger - use_team_logger() - - async def arun_member_agents( - task_description: str, expected_output: Optional[str] = None - ) -> AsyncIterator[str]: - """ - Send the same task to all the member agents and return the responses. - - Args: - task_description (str): The task description to send to the member agents. - expected_output (str): The expected output from the member agents. - - Returns: - str: The responses from the member agents. - """ - # Make sure for the member agent, we are using the agent logger - use_agent_logger() - self.memory = cast(TeamMemory, self.memory) - - # 2. Determine team context to send - team_context_str, team_member_interactions_str = _determine_team_context( - self, session_id - ) - - # 3. Create the member agent task - member_agent_task = self._formate_member_agent_task( - task_description, - expected_output, - team_context_str, - team_member_interactions_str, - ) - - # Create tasks for all member agents - tasks = [] - for member_agent_index, member_agent in enumerate(self.members): - # We cannot stream responses with async gather - current_agent = member_agent # Create a reference to the current agent - current_index = ( - member_agent_index # Create a reference to the current index - ) - self._initialize_member(current_agent, session_id=session_id) - - async def run_member_agent( - agent=current_agent, idx=current_index - ) -> str: - response = await agent.arun( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=False, - ) - check_if_run_cancelled(response) - - member_name = agent.name if agent.name else f"agent_{idx}" - self.memory = cast(TeamMemory, self.memory) - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - self.memory.add_interaction_to_team_context( - member_name=member_name, - task=task_description, - run_response=agent.run_response, - ) - else: - self.memory = cast(Memory, self.memory) - self.memory.add_interaction_to_team_context( - session_id=session_id, - member_name=member_name, - task=task_description, - run_response=agent.run_response, - ) - - # Add the member run to the team run response - self.run_response = cast(TeamRunResponse, self.run_response) - self.run_response.add_member_run(agent.run_response) - - # Update team session state - self._update_team_session_state(current_agent) - - # Update the team media - self._update_team_media(agent.run_response) - - try: - if response.content is None and ( - response.tools is None or len(response.tools) == 0 - ): - return f"Agent {member_name}: No response from the member agent." - elif isinstance(response.content, str): - if len(response.content.strip()) > 0: - return f"Agent {member_name}: {response.content}" - elif response.tools is not None and len(response.tools) > 0: - return f"Agent {member_name}: {','.join([tool.get('content') for tool in response.tools])}" - elif issubclass(type(response.content), BaseModel): - return f"Agent {member_name}: {response.content.model_dump_json(indent=2)}" # type: ignore - else: - import json - - return f"Agent {member_name}: {json.dumps(response.content, indent=2)}" - except Exception as e: - return f"Agent {member_name}: Error - {str(e)}" - - return f"Agent {member_name}: No Response" - - tasks.append(run_member_agent) - - # Need to collect and process yielded values from each task - results = await asyncio.gather(*[task() for task in tasks]) - for result in results: - yield result - - # Afterward, switch back to the team logger - use_team_logger() - - if async_mode: - run_member_agents_function = arun_member_agents # type: ignore - else: - run_member_agents_function = run_member_agents # type: ignore - - run_member_agents_func = Function.from_callable( - run_member_agents_function, strict=True - ) - - return run_member_agents_func - - def get_transfer_task_function( - self, - session_id: str, - stream: bool = False, - stream_intermediate_steps: bool = False, - async_mode: bool = False, - images: Optional[List[Image]] = None, - videos: Optional[List[Video]] = None, - audio: Optional[List[Audio]] = None, - files: Optional[List[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - ) -> Function: - if not images: - images = [] - if not videos: - videos = [] - if not audio: - audio = [] - if not files: - files = [] - - def _determine_team_context( - self, session_id: str - ) -> Tuple[Optional[str], Optional[str]]: - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - team_context_str = None - if self.enable_agentic_context: - team_context_str = self.memory.get_team_context_str() - - team_member_interactions_str = None - if self.share_member_interactions: - team_member_interactions_str = ( - self.memory.get_team_member_interactions_str() - ) - if context_images := self.memory.get_team_context_images(): - images.extend( - [Image.from_artifact(img) for img in context_images] - ) - if context_videos := self.memory.get_team_context_videos(): - videos.extend( - [Video.from_artifact(vid) for vid in context_videos] - ) - if context_audio := self.memory.get_team_context_audio(): - audio.extend( - [Audio.from_artifact(aud) for aud in context_audio] - ) - else: - self.memory = cast(Memory, self.memory) - team_context_str = None - if self.enable_agentic_context: - team_context_str = self.memory.get_team_context_str( - session_id=session_id - ) # type: ignore - - team_member_interactions_str = None - if self.share_member_interactions: - team_member_interactions_str = ( - self.memory.get_team_member_interactions_str( - session_id=session_id - ) - ) # type: ignore - if context_images := self.memory.get_team_context_images( - session_id=session_id - ): # type: ignore - images.extend( - [Image.from_artifact(img) for img in context_images] - ) - if context_videos := self.memory.get_team_context_videos( - session_id=session_id - ): # type: ignore - videos.extend( - [Video.from_artifact(vid) for vid in context_videos] - ) - if context_audio := self.memory.get_team_context_audio( - session_id=session_id - ): # type: ignore - audio.extend( - [Audio.from_artifact(aud) for aud in context_audio] - ) - return team_context_str, team_member_interactions_str - - def transfer_task_to_member( - member_id: str, task_description: str, expected_output: Optional[str] = None - ) -> Iterator[Union[RunResponseEvent, TeamRunResponseEvent, str]]: - """Use this function to transfer a task to the selected team member. - You must provide a clear and concise description of the task the member should achieve AND the expected output. - - Args: - member_id (str): The ID of the member to transfer the task to. - task_description (str): A clear and concise description of the task the member should achieve. - expected_output (str): The expected output from the member (optional). - Returns: - str: The result of the delegated task. - """ - # 1. Find the member agent using the helper function - result = self._find_member_by_id(member_id) - if result is None: - yield f"Member with ID {member_id} not found in the team or any subteams. Please choose the correct member from the list of members:\n\n{self.get_members_system_message_content(indent=0)}" - return - - member_agent_index, member_agent = result - self._initialize_member(member_agent, session_id=session_id) - - # 2. Determine team context to send - team_context_str, team_member_interactions_str = _determine_team_context( - self, session_id - ) - - # 3. Create the member agent task - member_agent_task = self._formate_member_agent_task( - task_description, - expected_output, - team_context_str, - team_member_interactions_str, - ) - - # Make sure for the member agent, we are using the agent logger - use_agent_logger() - - # Handle enable_agentic_knowledge_filters on the member agent - if ( - self.enable_agentic_knowledge_filters - and not member_agent.enable_agentic_knowledge_filters - ): - member_agent.enable_agentic_knowledge_filters = ( - self.enable_agentic_knowledge_filters - ) - - if stream: - member_agent_run_response_stream = member_agent.run( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - for member_agent_run_response_event in member_agent_run_response_stream: - check_if_run_cancelled(member_agent_run_response_event) - - # Yield the member event directly - yield member_agent_run_response_event - else: - member_agent_run_response = member_agent.run( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=False, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - - check_if_run_cancelled(member_agent_run_response) - - try: - if member_agent_run_response.content is None and ( - member_agent_run_response.tools is None - or len(member_agent_run_response.tools) == 0 - ): - yield "No response from the member agent." - elif isinstance(member_agent_run_response.content, str): - content = member_agent_run_response.content.strip() - if len(content) > 0: - yield content - - # If the content is empty but we have tool calls - elif ( - member_agent_run_response.tools is not None - and len(member_agent_run_response.tools) > 0 - ): - tool_str = "" - for tool in member_agent_run_response.tools: - if tool.result: - tool_str += f"{tool.result}," - yield tool_str.rstrip(",") - - elif issubclass(type(member_agent_run_response.content), BaseModel): - yield member_agent_run_response.content.model_dump_json( - indent=2 - ) # type: ignore - else: - import json - - yield json.dumps(member_agent_run_response.content, indent=2) - except Exception as e: - yield str(e) - - # Afterward, switch back to the team logger - use_team_logger() - - # Update the memory - member_name = ( - member_agent.name - if member_agent.name - else f"agent_{member_agent_index}" - ) - - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - self.memory.add_interaction_to_team_context( - member_name=member_name, - task=task_description, - run_response=member_agent.run_response, # type: ignore - ) - else: - self.memory = cast(Memory, self.memory) - self.memory.add_interaction_to_team_context( - session_id=session_id, - member_name=member_name, - task=task_description, - run_response=member_agent.run_response, # type: ignore - ) - - # Add the member run to the team run response - self.run_response = cast(TeamRunResponse, self.run_response) - self.run_response.add_member_run(member_agent.run_response) # type: ignore - - # Update team session state - self._update_team_session_state(member_agent) - - # Update the team media - self._update_team_media(member_agent.run_response) # type: ignore - - async def atransfer_task_to_member( - member_id: str, task_description: str, expected_output: Optional[str] = None - ) -> AsyncIterator[Union[RunResponseEvent, TeamRunResponseEvent, str]]: - """Use this function to transfer a task to the selected team member. - You must provide a clear and concise description of the task the member should achieve AND the expected output. - - Args: - member_id (str): The ID of the member to transfer the task to. - task_description (str): A clear and concise description of the task the member should achieve. - expected_output (str): The expected output from the member (optional). - Returns: - str: The result of the delegated task. - """ - - # Find the member agent using the helper function - result = self._find_member_by_id(member_id) - if result is None: - yield f"Member with ID {member_id} not found in the team or any subteams. Please choose the correct member from the list of members:\n\n{self.get_members_system_message_content(indent=0)}" - return - - member_agent_index, member_agent = result - self._initialize_member(member_agent, session_id=session_id) - - # 2. Determine team context to send - team_context_str, team_member_interactions_str = _determine_team_context( - self, session_id - ) - - # 3. Create the member agent task - member_agent_task = self._formate_member_agent_task( - task_description, - expected_output, - team_context_str, - team_member_interactions_str, - ) - - # Make sure for the member agent, we are using the agent logger - use_agent_logger() - - # Handle enable_agentic_knowledge_filters - if ( - self.enable_agentic_knowledge_filters - and not member_agent.enable_agentic_knowledge_filters - ): - member_agent.enable_agentic_knowledge_filters = ( - self.enable_agentic_knowledge_filters - ) - - if stream: - member_agent_run_response_stream = await member_agent.arun( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - async for ( - member_agent_run_response_event - ) in member_agent_run_response_stream: - check_if_run_cancelled(member_agent_run_response_event) - yield member_agent_run_response_event - else: - member_agent_run_response = await member_agent.arun( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=False, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - check_if_run_cancelled(member_agent_run_response) - - try: - if member_agent_run_response.content is None and ( - member_agent_run_response.tools is None - or len(member_agent_run_response.tools) == 0 - ): - yield "No response from the member agent." - elif isinstance(member_agent_run_response.content, str): - if len(member_agent_run_response.content.strip()) > 0: - yield member_agent_run_response.content - - # If the content is empty but we have tool calls - elif ( - member_agent_run_response.tools is not None - and len(member_agent_run_response.tools) > 0 - ): - yield ",".join( - [ - tool.result - for tool in member_agent_run_response.tools - if tool.result - ] - ) # type: ignore - elif issubclass(type(member_agent_run_response.content), BaseModel): - yield member_agent_run_response.content.model_dump_json( - indent=2 - ) # type: ignore - else: - import json - - yield json.dumps(member_agent_run_response.content, indent=2) - except Exception as e: - yield str(e) - - # Afterward, switch back to the team logger - use_team_logger() - - # Update the memory - member_name = ( - member_agent.name - if member_agent.name - else f"agent_{member_agent_index}" - ) - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - self.memory.add_interaction_to_team_context( - member_name=member_name, - task=task_description, - run_response=member_agent.run_response, # type: ignore - ) - else: - self.memory = cast(Memory, self.memory) - self.memory.add_interaction_to_team_context( - session_id=session_id, - member_name=member_name, - task=task_description, - run_response=member_agent.run_response, # type: ignore - ) - - # Add the member run to the team run response - self.run_response = cast(TeamRunResponse, self.run_response) - self.run_response.add_member_run(member_agent.run_response) # type: ignore - - # Update team session state - self._update_team_session_state(member_agent) - - # Update the team media - self._update_team_media(member_agent.run_response) # type: ignore - - if async_mode: - transfer_function = atransfer_task_to_member # type: ignore - else: - transfer_function = transfer_task_to_member # type: ignore - - transfer_func = Function.from_callable(transfer_function, strict=True) - - return transfer_func - - def _formate_member_agent_task( - self, - task_description: str, - expected_output: Optional[str] = None, - team_context_str: Optional[str] = None, - team_member_interactions_str: Optional[str] = None, - ) -> str: - member_agent_task = "You are a member of a team of agents. Your goal is to complete the following task:" - member_agent_task += f"\n\n\n{task_description}\n" - - if expected_output is not None: - member_agent_task += ( - f"\n\n\n{expected_output}\n" - ) - - if team_context_str: - member_agent_task += f"\n\n{team_context_str}" - if team_member_interactions_str: - member_agent_task += f"\n\n{team_member_interactions_str}" - - return member_agent_task - - def _get_member_id(self, member: Union[Agent, "Team"]) -> str: - """ - Get the ID of a member - """ - if ( - isinstance(member, Agent) - and member.agent_id is not None - and (not is_valid_uuid(member.agent_id)) - ): - url_safe_member_id = url_safe_string(member.agent_id) - elif ( - isinstance(member, Team) - and member.team_id is not None - and (not is_valid_uuid(member.team_id)) - ): - url_safe_member_id = url_safe_string(member.team_id) - elif member.name is not None: - url_safe_member_id = url_safe_string(member.name) - else: - url_safe_member_id = None - return url_safe_member_id - - def _find_member_by_id( - self, member_id: str - ) -> Optional[Tuple[int, Union[Agent, "Team"]]]: - """ - Recursively search through team members and subteams to find an agent by name. - - Args: - member_id (str): ID of the agent to find - - Returns: - Optional[Tuple[int, Union[Agent, "Team"], Optional[str]]]: Tuple containing: - - Index of the member in its immediate parent team - - The top-level leader agent - """ - # First check direct members - for i, member in enumerate(self.members): - if member.name is not None: - url_safe_member_id = self._get_member_id(member) - if url_safe_member_id == member_id: - return i, member - - # If this member is a team, search its members recursively - if isinstance(member, Team): - result = member._find_member_by_id(member_id) - if result is not None: - # Found in subteam, return with the top-level team member's name - return i, member - - return None - - def get_forward_task_function( - self, - message: Message, - session_id: str, - stream: bool = False, - stream_intermediate_steps: bool = False, - async_mode: bool = False, - images: Optional[Sequence[Image]] = None, - videos: Optional[Sequence[Video]] = None, - audio: Optional[Sequence[Audio]] = None, - files: Optional[Sequence[File]] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - ) -> Function: - if not images: - images = [] - if not videos: - videos = [] - if not audio: - audio = [] - if not files: - files = [] - - def forward_task_to_member( - member_id: str, expected_output: Optional[str] = None - ) -> Iterator[Union[RunResponseEvent, TeamRunResponseEvent, str]]: - """Use this function to forward the request to the selected team member. - Args: - member_id (str): The ID of the member to transfer the task to. - expected_output (str): The expected output from the member (optional). - Returns: - str: The result of the delegated task. - """ - self._member_response_model = None - - # Find the member agent using the helper function - result = self._find_member_by_id(member_id) - if result is None: - yield f"Member with ID {member_id} not found in the team or any subteams. Please choose the correct member from the list of members:\n\n{self.get_members_system_message_content(indent=0)}" - return - - member_agent_index, member_agent = result - self._initialize_member(member_agent, session_id=session_id) - - # Since we return the response directly from the member agent, we need to set the response model from the team down. - if not member_agent.response_model and self.response_model: - member_agent.response_model = self.response_model - - # If the member will produce structured output, we need to parse the response - if member_agent.response_model is not None: - self._member_response_model = member_agent.response_model - - # Make sure for the member agent, we are using the agent logger - use_agent_logger() - - # If found in subteam, include the path in the task description - member_agent_task = message.get_content_string() - - if expected_output: - member_agent_task += ( - f"\n\n\n{expected_output}\n" - ) - - # Handle enable_agentic_knowledge_filters - if ( - self.enable_agentic_knowledge_filters - and not member_agent.enable_agentic_knowledge_filters - ): - member_agent.enable_agentic_knowledge_filters = ( - self.enable_agentic_knowledge_filters - ) - - # 2. Get the response from the member agent - if stream: - member_agent_run_response_stream = member_agent.run( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - for member_agent_run_response_chunk in member_agent_run_response_stream: - check_if_run_cancelled(member_agent_run_response_chunk) - yield member_agent_run_response_chunk - else: - member_agent_run_response = member_agent.run( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=False, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - check_if_run_cancelled(member_agent_run_response) - - try: - if member_agent_run_response.content is None and ( - member_agent_run_response.tools is None - or len(member_agent_run_response.tools) == 0 - ): - yield "No response from the member agent." - elif isinstance(member_agent_run_response.content, str): - if len(member_agent_run_response.content.strip()) > 0: - yield member_agent_run_response.content - - # If the content is empty but we have tool calls - elif ( - member_agent_run_response.tools is not None - and len(member_agent_run_response.tools) > 0 - ): - tool_str = "" - for tool in member_agent_run_response.tools: - if tool.result: - tool_str += f"{tool.result}," - yield tool_str.rstrip(",") - - elif issubclass(type(member_agent_run_response.content), BaseModel): - yield member_agent_run_response.content.model_dump_json( - indent=2 - ) # type: ignore - else: - import json - - yield json.dumps(member_agent_run_response.content, indent=2) - except Exception as e: - yield str(e) - - # Afterward, switch back to the team logger - use_team_logger() - - # Update the memory - member_name = ( - member_agent.name - if member_agent.name - else f"agent_{member_agent_index}" - ) - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - self.memory.add_interaction_to_team_context( - member_name=member_name, - task=message.get_content_string(), - run_response=member_agent.run_response, # type: ignore - ) - else: - self.memory = cast(Memory, self.memory) - self.memory.add_interaction_to_team_context( - session_id=session_id, # type: ignore - member_name=member_name, - task=message.get_content_string(), - run_response=member_agent.run_response, # type: ignore - ) - - # Add the member run to the team run response - self.run_response = cast(TeamRunResponse, self.run_response) - self.run_response.add_member_run(member_agent.run_response) # type: ignore - - # Update team session state - self._update_team_session_state(member_agent) - - # Update the team media - self._update_team_media(member_agent.run_response) # type: ignore - - async def aforward_task_to_member( - member_id: str, expected_output: Optional[str] = None - ) -> AsyncIterator[Union[RunResponseEvent, TeamRunResponseEvent, str]]: - """Use this function to forward a message to the selected team member. - - Args: - member_id (str): The ID of the member to transfer the task to. - expected_output (str): The expected output from the member (optional). - Returns: - str: The result of the delegated task. - """ - self._member_response_model = None - - # Find the member agent using the helper function - result = self._find_member_by_id(member_id) - if result is None: - yield f"Member with ID {member_id} not found in the team or any subteams. Please choose the correct member from the list of members:\n\n{self.get_members_system_message_content(indent=0)}" - return - - member_agent_index, member_agent = result - self._initialize_member(member_agent, session_id=session_id) - - # If the member will produce structured output, we need to parse the response - if member_agent.response_model is not None: - self._member_response_model = member_agent.response_model - - # Make sure for the member agent, we are using the agent logger - use_agent_logger() - - # If found in subteam, include the path in the task description - member_agent_task = message.get_content_string() - - if expected_output: - member_agent_task += ( - f"\n\n\n{expected_output}\n" - ) - - # Handle enable_agentic_knowledge_filters - if ( - self.enable_agentic_knowledge_filters - and not member_agent.enable_agentic_knowledge_filters - ): - member_agent.enable_agentic_knowledge_filters = ( - self.enable_agentic_knowledge_filters - ) - - # 2. Get the response from the member agent - if stream: - member_agent_run_response_stream = await member_agent.arun( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=True, - stream_intermediate_steps=stream_intermediate_steps, - knowledge_filters=knowledge_filters - if not member_agent.knowledge_filters and member_agent.knowledge - else None, - ) - async for ( - member_agent_run_response_event - ) in member_agent_run_response_stream: - check_if_run_cancelled(member_agent_run_response_event) - yield member_agent_run_response_event - else: - member_agent_run_response = await member_agent.arun( - member_agent_task, - images=images, - videos=videos, - audio=audio, - files=files, - stream=False, - knowledge_filters=knowledge_filters - if (member_agent.knowledge_filters and member_agent.knowledge) - else None, - ) - - try: - if member_agent_run_response.content is None and ( - member_agent_run_response.tools is None - or len(member_agent_run_response.tools) == 0 - ): - yield "No response from the member agent." - elif isinstance(member_agent_run_response.content, str): - if len(member_agent_run_response.content.strip()) > 0: - yield member_agent_run_response.content - - # If the content is empty but we have tool calls - elif ( - member_agent_run_response.tools is not None - and len(member_agent_run_response.tools) > 0 - ): - yield ",".join( - [ - tool.result - for tool in member_agent_run_response.tools - if tool.result - ] - ) # type: ignore - elif issubclass(type(member_agent_run_response.content), BaseModel): - yield member_agent_run_response.content.model_dump_json( - indent=2 - ) # type: ignore - else: - import json - - yield json.dumps(member_agent_run_response.content, indent=2) - except Exception as e: - yield str(e) - - # Afterward, switch back to the team logger - use_team_logger() - - # Update the memory - member_name = ( - member_agent.name - if member_agent.name - else f"agent_{member_agent_index}" - ) - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - self.memory.add_interaction_to_team_context( - member_name=member_name, - task=message.get_content_string(), - run_response=member_agent.run_response, # type: ignore - ) - else: - self.memory = cast(Memory, self.memory) - self.memory.add_interaction_to_team_context( - session_id=session_id, # type: ignore - member_name=member_name, - task=message.get_content_string(), - run_response=member_agent.run_response, # type: ignore - ) - - # Add the member run to the team run response - self.run_response = cast(TeamRunResponse, self.run_response) - self.run_response.add_member_run(member_agent.run_response) # type: ignore - - # Update team session state - self._update_team_session_state(member_agent) - - # Update the team media - self._update_team_media(member_agent.run_response) # type: ignore - - if async_mode: - forward_function = aforward_task_to_member # type: ignore - else: - forward_function = forward_task_to_member # type: ignore - - forward_func = Function.from_callable(forward_function, strict=True) - - forward_func.stop_after_tool_call = True - forward_func.show_result = True - - return forward_func - - ########################################################################### - # Storage - ########################################################################### - - def read_from_storage(self, session_id: str) -> Optional[TeamSession]: - """Load the TeamSession from storage - - Returns: - Optional[TeamSession]: The loaded TeamSession or None if not found. - """ - if self.storage is not None and session_id is not None: - self.team_session = cast( - TeamSession, self.storage.read(session_id=session_id) - ) - if self.team_session is not None: - self.load_team_session(session=self.team_session) - else: - # New session, just reset the state - self.session_name = None - return self.team_session - - def write_to_storage( - self, session_id: str, user_id: Optional[str] = None - ) -> Optional[TeamSession]: - """Save the TeamSession to storage - - Returns: - Optional[TeamSession]: The saved TeamSession or None if not saved. - """ - if self.storage is not None: - self.team_session = cast( - TeamSession, - self.storage.upsert( - session=self._get_team_session( - session_id=session_id, user_id=user_id - ) - ), - ) - return self.team_session - - def rename_session( - self, session_name: str, session_id: Optional[str] = None - ) -> None: - """Rename the current session and save to storage""" - if self.session_id is None and session_id is None: - raise ValueError("Session ID is not initialized") - - session_id = session_id or self.session_id - - # -*- Read from storage - self.read_from_storage(session_id=session_id) # type: ignore - # -*- Rename session - self.session_name = session_name - # -*- Save to storage - self.write_to_storage(session_id=session_id, user_id=self.user_id) # type: ignore - # -*- Log Agent session - self._log_team_session(session_id=session_id, user_id=self.user_id) # type: ignore - - def delete_session(self, session_id: str) -> None: - """Delete the current session and save to storage""" - if self.storage is not None: - self.storage.delete_session(session_id=session_id) - - def load_team_session(self, session: TeamSession): - """Load the existing TeamSession from an TeamSession (from the database)""" - from agno.utils.merge_dict import merge_dictionaries - - # Get the team_id, user_id and session_id from the database - if self.team_id is None and session.team_id is not None: - self.team_id = session.team_id - if self.user_id is None and session.user_id is not None: - self.user_id = session.user_id - if self.session_id is None and session.session_id is not None: - self.session_id = session.session_id - - # Read team_data from the database - if session.team_data is not None: - # Get name from database and update the team name if not set - if self.name is None and "name" in session.team_data: - self.name = session.team_data.get("name") - - # Read session_data from the database - if session.session_data is not None: - # Get the session_name from database and update the current session_name if not set - if self.session_name is None and "session_name" in session.session_data: - self.session_name = session.session_data.get("session_name") - - # Get the session_state from the database and update the current session_state - if "session_state" in session.session_data: - session_state_from_db = session.session_data.get("session_state") - if ( - session_state_from_db is not None - and isinstance(session_state_from_db, dict) - and len(session_state_from_db) > 0 - ): - # If the session_state is already set, merge the session_state from the database with the current session_state - if self.session_state is not None and len(self.session_state) > 0: - # This updates session_state_from_db - merge_dictionaries(session_state_from_db, self.session_state) - # Update the current session_state - self.session_state = session_state_from_db - - if "team_session_state" in session.session_data: - team_session_state_from_db = session.session_data.get( - "team_session_state" - ) - if ( - team_session_state_from_db is not None - and isinstance(team_session_state_from_db, dict) - and len(team_session_state_from_db) > 0 - ): - # If the team_session_state is already set, merge the team_session_state from the database with the current team_session_state - if ( - self.team_session_state is not None - and len(self.team_session_state) > 0 - ): - # This updates team_session_state_from_db - merge_dictionaries( - team_session_state_from_db, self.team_session_state - ) - # Update the current team_session_state - self.team_session_state = team_session_state_from_db - - # Get the session_metrics from the database - if "session_metrics" in session.session_data: - session_metrics_from_db = session.session_data.get("session_metrics") - if session_metrics_from_db is not None and isinstance( - session_metrics_from_db, dict - ): - self.session_metrics = SessionMetrics(**session_metrics_from_db) - - # Get images, videos, and audios from the database - if "images" in session.session_data: - images_from_db = session.session_data.get("images") - if images_from_db is not None and isinstance(images_from_db, list): - if self.images is None: - self.images = [] - self.images.extend( - [ImageArtifact.model_validate(img) for img in images_from_db] - ) - if "videos" in session.session_data: - videos_from_db = session.session_data.get("videos") - if videos_from_db is not None and isinstance(videos_from_db, list): - if self.videos is None: - self.videos = [] - self.videos.extend( - [VideoArtifact.model_validate(vid) for vid in videos_from_db] - ) - if "audio" in session.session_data: - audio_from_db = session.session_data.get("audio") - if audio_from_db is not None and isinstance(audio_from_db, list): - if self.audio is None: - self.audio = [] - self.audio.extend( - [AudioArtifact.model_validate(aud) for aud in audio_from_db] - ) - - # Read extra_data from the database - if session.extra_data is not None: - # If extra_data is set in the agent, update the database extra_data with the agent's extra_data - if self.extra_data is not None: - # Updates agent_session.extra_data in place - merge_dictionaries(session.extra_data, self.extra_data) - # Update the current extra_data with the extra_data from the database which is updated in place - self.extra_data = session.extra_data - - if self.memory is None: - self.memory = session.memory # type: ignore - - if not (isinstance(self.memory, TeamMemory) or isinstance(self.memory, Memory)): - # Is it a dict of `TeamMemory`? - if isinstance(self.memory, dict) and "create_user_memories" in self.memory: - # Convert dict to TeamMemory - self.memory = TeamMemory(**self.memory) - else: - # Default to base memory - self.memory = Memory() - - if session.memory is not None: - if isinstance(self.memory, TeamMemory): - try: - if "runs" in session.memory: - try: - self.memory.runs = [ - TeamRun.from_dict(m) for m in session.memory["runs"] - ] - except Exception as e: - log_warning(f"Failed to load runs from memory: {e}") - if "messages" in session.memory: - try: - self.memory.messages = [ - Message.model_validate(m) - for m in session.memory["messages"] - ] - except Exception as e: - log_warning(f"Failed to load messages from memory: {e}") - if "memories" in session.memory: - from agno.memory.memory import Memory as UserMemoryV1 - - try: - self.memory.memories = [ - UserMemoryV1.model_validate(m) - for m in session.memory["memories"] - ] - except Exception as e: - log_warning(f"Failed to load user memories: {e}") - - if self.memory.create_user_memories: - if self.user_id is not None and self.memory.user_id is None: - self.memory.user_id = self.user_id - - self.memory.load_user_memories() - if self.user_id is not None: - log_debug(f"Memories loaded for user: {self.user_id}") - else: - log_debug("Memories loaded") - - except Exception as e: - log_warning(f"Failed to load TeamMemory: {e}") - elif isinstance(self.memory, Memory): - if "runs" in session.memory: - try: - if self.memory.runs is None: - self.memory.runs = {} - self.memory.runs[session.session_id] = [] - for run in session.memory["runs"]: - run_session_id = run["session_id"] - if "team_id" in run: - self.memory.runs[run_session_id].append( - TeamRunResponse.from_dict(run) - ) - else: - self.memory.runs[run_session_id].append( - RunResponse.from_dict(run) - ) - except Exception as e: - log_warning(f"Failed to load runs from memory: {e}") - if "team_context" in session.memory: - from agno.memory.v2.memory import TeamContext - - try: - self.memory.team_context = { - session_id: TeamContext.from_dict(team_context) - for session_id, team_context in session.memory[ - "team_context" - ].items() - } - except Exception as e: - log_warning(f"Failed to load team context: {e}") - if "memories" in session.memory: - if self.memory.memories is not None: - pass - else: - from agno.memory.v2.memory import UserMemory as UserMemoryV2 - - try: - self.memory.memories = { - user_id: { - memory_id: UserMemoryV2.from_dict(memory) - for memory_id, memory in user_memories.items() - } - for user_id, user_memories in session.memory[ - "memories" - ].items() - } - except Exception as e: - log_warning(f"Failed to load user memories: {e}") - if "summaries" in session.memory: - if self.memory.summaries is not None: - pass - else: - from agno.memory.v2.memory import ( - SessionSummary as SessionSummaryV2, - ) - - try: - self.memory.summaries = { - user_id: { - session_id: SessionSummaryV2.from_dict(summary) - for session_id, summary in user_session_summaries.items() - } - for user_id, user_session_summaries in session.memory[ - "summaries" - ].items() - } - except Exception as e: - log_warning(f"Failed to load session summaries: {e}") - log_debug(f"-*- TeamSession loaded: {session.session_id}") - - def load_session(self, force: bool = False) -> Optional[str]: - """Load an existing session from the database and return the session_id. - If a session does not exist, create a new session. - - - If a session exists in the database, load the session. - - If a session does not exist in the database, create a new session. - """ - # If a team_session is already loaded, return the session_id from the team_session - # if the session_id matches the session_id from the team_session - if self.team_session is not None and not force: - if ( - self.session_id is not None - and self.team_session.session_id == self.session_id - ): - return self.team_session.session_id - - # Load an existing session or create a new session - if self.storage is not None: - # Load existing session if session_id is provided - log_debug(f"Reading TeamSession: {self.session_id}") - self.read_from_storage(session_id=self.session_id) # type: ignore - - # Create a new session if it does not exist - if self.team_session is None: - log_debug("-*- Creating new TeamSession") - if self.session_id is None or self.session_id == "": - self.session_id = str(uuid4()) - if self.team_id is None: - self.initialize_team(session_id=self.session_id) - # write_to_storage() will create a new TeamSession - # and populate self.team_session with the new session - self.write_to_storage(session_id=self.session_id, user_id=self.user_id) # type: ignore - if self.team_session is None: - raise Exception("Failed to create new TeamSession in storage") - log_debug(f"-*- Created TeamSession: {self.team_session.session_id}") - self._log_team_session(session_id=self.session_id, user_id=self.user_id) # type: ignore - return self.session_id - - def get_messages_for_session( - self, session_id: Optional[str] = None, user_id: Optional[str] = None - ) -> List[Message]: - """Get messages for a session""" - _session_id = session_id or self.session_id - _user_id = user_id or self.user_id - if _session_id is None: - log_warning("Session ID is not set, cannot get messages for session") - return [] - - if self.memory is None: - self.read_from_storage(session_id=_session_id) - - if self.memory is None: - return [] - - if isinstance(self.memory, AgentMemory): - return self.memory.messages - elif isinstance(self.memory, Memory): - return self.memory.get_messages_from_last_n_runs(session_id=_session_id) - else: - return [] - - def get_session_summary( - self, session_id: Optional[str] = None, user_id: Optional[str] = None - ): - """Get the session summary for the given session ID and user ID.""" - if self.memory is None: - return None - - session_id = session_id if session_id is not None else self.session_id - if session_id is None: - raise ValueError("Session ID is required") - - if isinstance(self.memory, Memory): - user_id = user_id if user_id is not None else self.user_id - if user_id is None: - user_id = "default" - return self.memory.get_session_summary( - session_id=session_id, user_id=user_id - ) - elif isinstance(self.memory, TeamMemory): - raise ValueError("TeamMemory does not support get_session_summary") - else: - raise ValueError(f"Memory type {type(self.memory)} not supported") - - def get_user_memories(self, user_id: Optional[str] = None): - """Get the user memories for the given user ID.""" - if self.memory is None: - return None - user_id = user_id if user_id is not None else self.user_id - if user_id is None: - user_id = "default" - - if isinstance(self.memory, Memory): - return self.memory.get_user_memories(user_id=user_id) - elif isinstance(self.memory, TeamMemory): - raise ValueError("TeamMemory does not support get_user_memories") - else: - raise ValueError(f"Memory type {type(self.memory)} not supported") - - ########################################################################### - # Handle images, videos and audio - ########################################################################### - - def add_image(self, image: ImageArtifact) -> None: - if self.images is None: - self.images = [] - self.images.append(image) - if self.run_response is not None: - if self.run_response.images is None: - self.run_response.images = [] - self.run_response.images.append(image) - - def add_video(self, video: VideoArtifact) -> None: - if self.videos is None: - self.videos = [] - self.videos.append(video) - if self.run_response is not None: - if self.run_response.videos is None: - self.run_response.videos = [] - self.run_response.videos.append(video) - - def add_audio(self, audio: AudioArtifact) -> None: - if self.audio is None: - self.audio = [] - self.audio.append(audio) - if self.run_response is not None: - if self.run_response.audio is None: - self.run_response.audio = [] - self.run_response.audio.append(audio) - - def get_images(self) -> Optional[List[ImageArtifact]]: - return self.images - - def get_videos(self) -> Optional[List[VideoArtifact]]: - return self.videos - - def get_audio(self) -> Optional[List[AudioArtifact]]: - return self.audio - - def update_reasoning_content_from_tool_call( - self, run_response: TeamRunResponse, tool_name: str, tool_args: Dict[str, Any] - ) -> Optional[ReasoningStep]: - """Update reasoning_content based on tool calls that look like thinking or reasoning tools.""" - - # Case 1: ReasoningTools.think (has title, thought, optional action and confidence) - if ( - tool_name.lower() == "think" - and "title" in tool_args - and "thought" in tool_args - ): - title = tool_args["title"] - thought = tool_args["thought"] - action = tool_args.get("action", "") - confidence = tool_args.get("confidence", None) - - # Create a reasoning step - reasoning_step = ReasoningStep( - title=title, - reasoning=thought, - action=action, - next_action=NextAction.CONTINUE, - confidence=confidence, - ) - - # Add the step to the run response - self._add_reasoning_step_to_extra_data(run_response, reasoning_step) - - formatted_content = f"## {title}\n{thought}\n" - if action: - formatted_content += f"Action: {action}\n" - if confidence is not None: - formatted_content += f"Confidence: {confidence}\n" - formatted_content += "\n" - - self._append_to_reasoning_content(run_response, formatted_content) - return reasoning_step - - # Case 2: ReasoningTools.analyze (has title, result, analysis, optional next_action and confidence) - elif tool_name.lower() == "analyze" and "title" in tool_args: - title = tool_args["title"] - result = tool_args.get("result", "") - analysis = tool_args.get("analysis", "") - next_action = tool_args.get("next_action", "") - confidence = tool_args.get("confidence", None) - - # Map string next_action to enum - next_action_enum = NextAction.CONTINUE - if next_action.lower() == "validate": - next_action_enum = NextAction.VALIDATE - elif next_action.lower() in ["final", "final_answer", "finalize"]: - next_action_enum = NextAction.FINAL_ANSWER - - # Create a reasoning step - reasoning_step = ReasoningStep( - title=title, - result=result, - reasoning=analysis, - next_action=next_action_enum, - confidence=confidence, - ) - - # Add the step to the run response - self._add_reasoning_step_to_extra_data(run_response, reasoning_step) - - formatted_content = f"## {title}\n" - if result: - formatted_content += f"Result: {result}\n" - if analysis: - formatted_content += f"{analysis}\n" - if next_action and next_action.lower() != "continue": - formatted_content += f"Next Action: {next_action}\n" - if confidence is not None: - formatted_content += f"Confidence: {confidence}\n" - formatted_content += "\n" - - self._append_to_reasoning_content(run_response, formatted_content) - return reasoning_step - - # Case 3: ThinkingTools.think (simple format, just has 'thought') - elif tool_name.lower() == "think" and "thought" in tool_args: - thought = tool_args["thought"] - reasoning_step = ReasoningStep( - title="Thinking", - reasoning=thought, - confidence=None, - ) - formatted_content = f"## Thinking\n{thought}\n\n" - self._add_reasoning_step_to_extra_data(run_response, reasoning_step) - self._append_to_reasoning_content(run_response, formatted_content) - return reasoning_step - - return None - - def _append_to_reasoning_content( - self, run_response: TeamRunResponse, content: str - ) -> None: - """Helper to append content to the reasoning_content field.""" - if ( - not hasattr(run_response, "reasoning_content") - or not run_response.reasoning_content - ): # type: ignore - run_response.reasoning_content = content # type: ignore - else: - run_response.reasoning_content += content # type: ignore - - def _add_reasoning_step_to_extra_data( - self, run_response: TeamRunResponse, reasoning_step: ReasoningStep - ) -> None: - if run_response.extra_data is None: - from agno.run.response import RunResponseExtraData - - run_response.extra_data = RunResponseExtraData() - - if run_response.extra_data.reasoning_steps is None: - run_response.extra_data.reasoning_steps = [] - - run_response.extra_data.reasoning_steps.append(reasoning_step) - - def _add_reasoning_metrics_to_extra_data( - self, run_response: TeamRunResponse, reasoning_time_taken: float - ) -> None: - try: - if run_response.extra_data is None: - from agno.run.response import RunResponseExtraData - - run_response.extra_data = RunResponseExtraData() - - # Initialize reasoning_messages if it doesn't exist - if run_response.extra_data.reasoning_messages is None: - run_response.extra_data.reasoning_messages = [] - - metrics_message = Message( - role="assistant", - content=run_response.reasoning_content, - metrics={"time": reasoning_time_taken}, - ) - - # Add the metrics message to the reasoning_messages - run_response.extra_data.reasoning_messages.append(metrics_message) - except Exception as e: - log_error(f"Failed to add reasoning metrics to extra_data: {str(e)}") - - ########################################################################### - # Knowledge - ########################################################################### - - def get_relevant_docs_from_knowledge( - self, - query: str, - num_documents: Optional[int] = None, - filters: Optional[Dict[str, Any]] = None, - **kwargs, - ) -> Optional[List[Union[Dict[str, Any], str]]]: - """Return a list of references from the knowledge base""" - from agno.document import Document - - # Validate the filters against known valid filter keys - if self.knowledge is not None: - valid_filters, invalid_keys = self.knowledge.validate_filters(filters) # type: ignore - - # Warn about invalid filter keys - if invalid_keys: - # type: ignore - log_warning( - f"Invalid filter keys provided: {invalid_keys}. These filters will be ignored." - ) - log_info( - f"Valid filter keys are: {self.knowledge.valid_metadata_filters}" - ) # type: ignore - - # Only use valid filters - filters = valid_filters - if not filters: - log_warning( - "No valid filters remain after validation. Search will proceed without filters." - ) - - if self.retriever is not None and callable(self.retriever): - from inspect import signature - - try: - sig = signature(self.retriever) - retriever_kwargs: Dict[str, Any] = {} - if "team" in sig.parameters: - retriever_kwargs = {"team": self} - if "filters" in sig.parameters: - retriever_kwargs["filters"] = filters - retriever_kwargs.update( - {"query": query, "num_documents": num_documents, **kwargs} - ) - return self.retriever(**retriever_kwargs) - except Exception as e: - log_warning(f"Retriever failed: {e}") - raise e - try: - if self.knowledge is None or self.knowledge.vector_db is None: - return None - - if num_documents is None: - num_documents = self.knowledge.num_documents - - log_debug(f"Searching knowledge base with filters: {filters}") - relevant_docs: List[Document] = self.knowledge.search( - query=query, num_documents=num_documents, filters=filters - ) - - if not relevant_docs or len(relevant_docs) == 0: - log_debug("No relevant documents found for query") - return None - - return [doc.to_dict() for doc in relevant_docs] - except Exception as e: - log_warning(f"Error searching knowledge base: {e}") - raise e - - async def aget_relevant_docs_from_knowledge( - self, - query: str, - num_documents: Optional[int] = None, - filters: Optional[Dict[str, Any]] = None, - **kwargs, - ) -> Optional[List[Union[Dict[str, Any], str]]]: - """Get relevant documents from knowledge base asynchronously.""" - from agno.document import Document - - # Validate the filters against known valid filter keys - if self.knowledge is not None: - valid_filters, invalid_keys = self.knowledge.validate_filters(filters) # type: ignore - - # Warn about invalid filter keys - if invalid_keys: - # type: ignore - log_warning( - f"Invalid filter keys provided: {invalid_keys}. These filters will be ignored." - ) - # type: ignore - log_info( - f"Valid filter keys are: {self.knowledge.valid_metadata_filters}" - ) - - # Only use valid filters - filters = valid_filters - if not filters: - log_warning( - "No valid filters remain after validation. Search will proceed without filters." - ) - - if self.retriever is not None and callable(self.retriever): - from inspect import signature - - try: - sig = signature(self.retriever) - retriever_kwargs: Dict[str, Any] = {} - if "team" in sig.parameters: - retriever_kwargs = {"team": self} - if "filters" in sig.parameters: - retriever_kwargs["filters"] = filters - retriever_kwargs.update( - {"query": query, "num_documents": num_documents, **kwargs} - ) - return self.retriever(**retriever_kwargs) - except Exception as e: - log_warning(f"Retriever failed: {e}") - raise e - - try: - if self.knowledge is None or self.knowledge.vector_db is None: - return None - - if num_documents is None: - num_documents = self.knowledge.num_documents - - log_debug(f"Searching knowledge base with filters: {filters}") - relevant_docs: List[Document] = await self.knowledge.async_search( - query=query, num_documents=num_documents, filters=filters - ) - - if not relevant_docs or len(relevant_docs) == 0: - log_debug("No relevant documents found for query") - return None - - return [doc.to_dict() for doc in relevant_docs] - except Exception as e: - log_warning(f"Error searching knowledge base: {e}") - raise e - - def _convert_documents_to_string( - self, docs: List[Union[Dict[str, Any], str]] - ) -> str: - if docs is None or len(docs) == 0: - return "" - - if self.references_format == "yaml": - import yaml - - return yaml.dump(docs) - - import json - - return json.dumps(docs, indent=2) - - def _get_team_effective_filters( - self, knowledge_filters: Optional[Dict[str, Any]] = None - ) -> Optional[Dict[str, Any]]: - """ - Determine effective filters for the team, considering: - 1. Team-level filters (self.knowledge_filters) - 2. Run-time filters (knowledge_filters) - - Priority: Run-time filters > Team filters - """ - effective_filters = None - - # Start with team-level filters if they exist - if self.knowledge_filters: - effective_filters = self.knowledge_filters.copy() - - # Apply run-time filters if they exist - if knowledge_filters: - if effective_filters: - effective_filters.update(knowledge_filters) - else: - effective_filters = knowledge_filters - - return effective_filters - - def search_knowledge_base_function( - self, - knowledge_filters: Optional[Dict[str, Any]] = None, - async_mode: bool = False, - ) -> Callable: - """Factory function to create a search_knowledge_base function with filters.""" - - def search_knowledge_base(query: str) -> str: - """Use this function to search the knowledge base for information about a query. - - Args: - query: The query to search for. - - Returns: - str: A string containing the response from the knowledge base. - """ - # Get the relevant documents from the knowledge base, passing filters - self.run_response = cast(TeamRunResponse, self.run_response) - retrieval_timer = Timer() - retrieval_timer.start() - docs_from_knowledge = self.get_relevant_docs_from_knowledge( - query=query, filters=knowledge_filters - ) - if docs_from_knowledge is not None: - references = MessageReferences( - query=query, - references=docs_from_knowledge, - time=round(retrieval_timer.elapsed, 4), - ) - # Add the references to the run_response - if self.run_response.extra_data is None: - self.run_response.extra_data = RunResponseExtraData() - if self.run_response.extra_data.references is None: - self.run_response.extra_data.references = [] - self.run_response.extra_data.references.append(references) - retrieval_timer.stop() - log_debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s") - - if docs_from_knowledge is None: - return "No documents found" - return self._convert_documents_to_string(docs_from_knowledge) - - async def asearch_knowledge_base(query: str) -> str: - """Use this function to search the knowledge base for information about a query asynchronously. - - Args: - query: The query to search for. - - Returns: - str: A string containing the response from the knowledge base. - """ - self.run_response = cast(TeamRunResponse, self.run_response) - retrieval_timer = Timer() - retrieval_timer.start() - docs_from_knowledge = await self.aget_relevant_docs_from_knowledge( - query=query, filters=knowledge_filters - ) - if docs_from_knowledge is not None: - references = MessageReferences( - query=query, - references=docs_from_knowledge, - time=round(retrieval_timer.elapsed, 4), - ) - if self.run_response.extra_data is None: - self.run_response.extra_data = RunResponseExtraData() - if self.run_response.extra_data.references is None: - self.run_response.extra_data.references = [] - self.run_response.extra_data.references.append(references) - retrieval_timer.stop() - log_debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s") - - if docs_from_knowledge is None: - return "No documents found" - return self._convert_documents_to_string(docs_from_knowledge) - - if async_mode: - return asearch_knowledge_base - else: - return search_knowledge_base - - def search_knowledge_base_with_agentic_filters_function( - self, - knowledge_filters: Optional[Dict[str, Any]] = None, - async_mode: bool = False, - ) -> Callable: - """Factory function to create a search_knowledge_base function with filters.""" - - def search_knowledge_base( - query: str, filters: Optional[Dict[str, Any]] = None - ) -> str: - """Use this function to search the knowledge base for information about a query. - - Args: - query: The query to search for. - filters: The filters to apply to the search. This is a dictionary of key-value pairs. - - Returns: - str: A string containing the response from the knowledge base. - """ - search_filters = self._get_agentic_or_user_search_filters( - filters, knowledge_filters - ) - - # Get the relevant documents from the knowledge base, passing filters - self.run_response = cast(TeamRunResponse, self.run_response) - retrieval_timer = Timer() - retrieval_timer.start() - docs_from_knowledge = self.get_relevant_docs_from_knowledge( - query=query, filters=search_filters - ) - if docs_from_knowledge is not None: - references = MessageReferences( - query=query, - references=docs_from_knowledge, - time=round(retrieval_timer.elapsed, 4), - ) - # Add the references to the run_response - if self.run_response.extra_data is None: - self.run_response.extra_data = RunResponseExtraData() - if self.run_response.extra_data.references is None: - self.run_response.extra_data.references = [] - self.run_response.extra_data.references.append(references) - retrieval_timer.stop() - log_debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s") - - if docs_from_knowledge is None: - return "No documents found" - return self._convert_documents_to_string(docs_from_knowledge) - - async def asearch_knowledge_base( - query: str, filters: Optional[Dict[str, Any]] = None - ) -> str: - """Use this function to search the knowledge base for information about a query asynchronously. - - Args: - query: The query to search for. - filters: The filters to apply to the search. This is a dictionary of key-value pairs. - - Returns: - str: A string containing the response from the knowledge base. - """ - search_filters = self._get_agentic_or_user_search_filters( - filters, knowledge_filters - ) - - self.run_response = cast(TeamRunResponse, self.run_response) - retrieval_timer = Timer() - retrieval_timer.start() - docs_from_knowledge = await self.aget_relevant_docs_from_knowledge( - query=query, filters=search_filters - ) - if docs_from_knowledge is not None: - references = MessageReferences( - query=query, - references=docs_from_knowledge, - time=round(retrieval_timer.elapsed, 4), - ) - if self.run_response.extra_data is None: - self.run_response.extra_data = RunResponseExtraData() - if self.run_response.extra_data.references is None: - self.run_response.extra_data.references = [] - self.run_response.extra_data.references.append(references) - retrieval_timer.stop() - log_debug(f"Time to get references: {retrieval_timer.elapsed:.4f}s") - - if docs_from_knowledge is None: - return "No documents found" - return self._convert_documents_to_string(docs_from_knowledge) - - if async_mode: - return asearch_knowledge_base - else: - return search_knowledge_base - - ########################################################################### - # Logging - ########################################################################### - - def _create_run_data(self) -> Dict[str, Any]: - """Create and return the run data dictionary.""" - run_response_format = "text" - if self.response_model is not None: - run_response_format = "json" - elif self.markdown: - run_response_format = "markdown" - - functions = {} - if self._functions_for_model: - functions = { - f_name: func.to_dict() - for f_name, func in self._functions_for_model.items() - if isinstance(func, Function) - } - - run_data: Dict[str, Any] = { - "functions": functions, - "metrics": self.run_response.metrics, # type: ignore - } - - if self.monitoring: - run_data.update( - { - "run_input": self.run_input, - "run_response": self.run_response.to_dict(), # type: ignore - "run_response_format": run_response_format, - } - ) - - return run_data - - def _get_team_data(self) -> Dict[str, Any]: - team_data: Dict[str, Any] = {} - if self.name is not None: - team_data["name"] = self.name - if self.team_id is not None: - team_data["team_id"] = self.team_id - if self.model is not None: - team_data["model"] = self.model.to_dict() - if self.mode is not None: - team_data["mode"] = self.mode - return team_data - - def _get_session_data(self) -> Dict[str, Any]: - session_data: Dict[str, Any] = {} - if self.session_name is not None: - session_data["session_name"] = self.session_name - if self.session_state is not None and len(self.session_state) > 0: - session_data["session_state"] = self.session_state - if self.team_session_state is not None and len(self.team_session_state) > 0: - session_data["team_session_state"] = self.team_session_state - if self.session_metrics is not None: - session_data["session_metrics"] = ( - asdict(self.session_metrics) - if self.session_metrics is not None - else None - ) - if self.images is not None: - session_data["images"] = [img.to_dict() for img in self.images] # type: ignore - if self.videos is not None: - session_data["videos"] = [vid.to_dict() for vid in self.videos] # type: ignore - if self.audio is not None: - session_data["audio"] = [aud.to_dict() for aud in self.audio] # type: ignore - return session_data - - def _get_team_session( - self, session_id: str, user_id: Optional[str] = None - ) -> TeamSession: - from time import time - - """Get an TeamMemory object, which can be saved to the database""" - memory_dict = None - if self.memory is not None: - if isinstance(self.memory, TeamMemory): - self.memory = cast(TeamMemory, self.memory) - memory_dict = self.memory.to_dict() - else: - self.memory = cast(Memory, self.memory) - # We fake the structure on storage, to maintain the interface with the legacy implementation - if self.memory.runs is not None: - memory_dict = self.memory.to_dict() - run_responses = self.memory.runs.get(session_id) - if run_responses is not None: - memory_dict["runs"] = [rr.to_dict() for rr in run_responses] - - return TeamSession( - session_id=session_id, - team_id=self.team_id, - user_id=user_id, - team_session_id=self.team_session_id, - memory=memory_dict, - team_data=self._get_team_data(), - session_data=self._get_session_data(), - extra_data=self.extra_data, - created_at=int(time()), - ) - - def _log_team_run(self, session_id: str, user_id: Optional[str] = None) -> None: - if not self.telemetry and not self.monitoring: - return - - from agno.api.team import TeamRunCreate, create_team_run - - try: - run_data = self._create_run_data() - team_session: TeamSession = self.team_session or self._get_team_session( - session_id=session_id, user_id=user_id - ) - - create_team_run( - run=TeamRunCreate( - run_id=self.run_id, # type: ignore - run_data=run_data, - team_session_id=team_session.team_session_id, - session_id=team_session.session_id, - team_data=team_session.to_dict() - if self.monitoring - else team_session.telemetry_data(), - ), - monitor=self.monitoring, - ) - except Exception as e: - log_debug(f"Could not create team event: {e}") - - async def _alog_team_run( - self, session_id: str, user_id: Optional[str] = None - ) -> None: - if not self.telemetry and not self.monitoring: - return - - from agno.api.team import TeamRunCreate, acreate_team_run - - try: - run_data = self._create_run_data() - team_session: TeamSession = self.team_session or self._get_team_session( - session_id=session_id, user_id=user_id - ) - - await acreate_team_run( - run=TeamRunCreate( - run_id=self.run_id, - run_data=run_data, - session_id=team_session.session_id, - team_data=team_session.to_dict() - if self.monitoring - else team_session.telemetry_data(), - ), - monitor=self.monitoring, - ) - except Exception as e: - log_debug(f"Could not create team event: {e}") - - def _log_team_session(self, session_id: str, user_id: Optional[str] = None): - if not (self.telemetry or self.monitoring): - return - - from agno.api.team import TeamSessionCreate, upsert_team_session - - try: - team_session: TeamSession = self.team_session or self._get_team_session( - session_id=session_id, user_id=user_id - ) - upsert_team_session( - session=TeamSessionCreate( - session_id=team_session.session_id, - team_data=team_session.to_dict() - if self.monitoring - else team_session.telemetry_data(), - ), - monitor=self.monitoring, - ) - except Exception as e: - log_debug(f"Could not create team monitor: {e}") - - def _get_agentic_or_user_search_filters( - self, - filters: Optional[Dict[str, Any]], - effective_filters: Optional[Dict[str, Any]], - ) -> Dict[str, Any]: - """Helper function to determine the final filters to use for the search. - - Args: - filters: Filters passed by the agent. - effective_filters: Filters passed by user. - - Returns: - Dict[str, Any]: The final filters to use for the search. - """ - search_filters = {} - - # If agentic filters exist and manual filters (passed by user) do not, use agentic filters - if filters and not effective_filters: - search_filters = filters - - # If both agentic filters exist and manual filters (passed by user) exist, use manual filters (give priority to user and override) - if filters and effective_filters: - search_filters = effective_filters - - log_info(f"Filters used by Agent: {search_filters}") - return search_filters - - def register_team(self) -> None: - self._set_monitoring() - if not self.monitoring: - return - - from agno.api.team import TeamCreate, create_team - - try: - create_team( - team=TeamCreate( - team_id=self.team_id, - name=self.name, - config=self.to_platform_dict(), - parent_team_id=self.parent_team_id, - app_id=self.app_id, - workflow_id=self.workflow_id, - ), - ) - - except Exception as e: - log_debug(f"Could not create team on platform: {e}") - print(f"Could not create team on platform: {e}") - - async def _aregister_team(self) -> None: - self._set_monitoring() - if not self.monitoring: - return - - from agno.api.team import TeamCreate, acreate_team - - try: - await acreate_team( - team=TeamCreate( - team_id=self.team_id, - name=self.name, - config=self.to_platform_dict(), - parent_team_id=self.parent_team_id, - app_id=self.app_id, - workflow_id=self.workflow_id, - ), - ) - except Exception as e: - print(f"Could not create team on platform: {e}") - log_debug(f"Could not create team on platform: {e}") - - def to_platform_dict(self) -> Dict[str, Any]: - model = None - if self.model is not None: - model = { - "name": self.model.__class__.__name__, - "model": self.model.id, - "provider": self.model.provider, - } - tools: List[Dict[str, Any]] = [] - if self.tools is not None: - if not hasattr(self, "_tools_for_model") or self._tools_for_model is None: - team_model = self.model - if team_model is not None: - self.session_id = cast(str, self.session_id) - self.determine_tools_for_model( - model=team_model, session_id=self.session_id - ) - - if self._tools_for_model is not None: - for tool in self._tools_for_model: - if isinstance(tool, dict) and tool.get("type") == "function": - tools.append(tool["function"]) - payload = { - "members": [ - { - **( - member.get_agent_config_dict() - if isinstance(member, Agent) - else member.to_platform_dict() - if isinstance(member, Team) - else {} - ), - "agent_id": member.agent_id - if hasattr(member, "agent_id") - else None, - "team_id": member.team_id if hasattr(member, "team_id") else None, - "members": ( - [ - { - **( - sub_member.get_agent_config_dict() - if isinstance(sub_member, Agent) - else sub_member.to_platform_dict() - if isinstance(sub_member, Team) - else {} - ), - "agent_id": sub_member.agent_id - if hasattr(sub_member, "agent_id") - else None, - "team_id": sub_member.team_id - if hasattr(sub_member, "team_id") - else None, - } - for sub_member in member.members - if sub_member is not None - ] - if isinstance(member, Team) and hasattr(member, "members") - else [] - ), - } - for member in self.members - if member is not None - ], - "mode": self.mode, - "model": model, - "tools": tools, - "name": self.name, - "instructions": self.instructions, - "description": self.description, - "storage": { - "name": self.storage.__class__.__name__, - } - if self.storage is not None - else None, - # "tools": [tool.to_dict() for tool in self.tools] if self.tools is not None else None, - "memory": ( - { - "name": self.memory.__class__.__name__, - "model": ( - { - "name": self.memory.model.__class__.__name__, - "model": self.memory.model.id, - "provider": self.memory.model.provider, - } - if hasattr(self.memory, "model") - and self.memory.model is not None - else ( - { - "name": self.model.__class__.__name__, - "model": self.model.id, - "provider": self.model.provider, - } - if self.model is not None - else None - ) - ), - "db": ( - { - "name": self.memory.db.__class__.__name__, - "table_name": self.memory.db.table_name - if hasattr(self.memory.db, "table_name") - else None, - "db_url": self.memory.db.db_url - if hasattr(self.memory.db, "db_url") - else None, - } - if hasattr(self.memory, "db") and self.memory.db is not None - else None - ), - } - if self.memory is not None - and hasattr(self.memory, "db") - and self.memory.db is not None - else None - ), - } - payload = {k: v for k, v in payload.items() if v is not None} - return payload diff --git a/isek/tools/calculator.py b/isek/tools/calculator.py deleted file mode 100644 index 7f6e296..0000000 --- a/isek/tools/calculator.py +++ /dev/null @@ -1,35 +0,0 @@ -from isek.tools.toolkit import Toolkit - - -def add_numbers(a: int, b: int) -> int: - """Add two numbers together.""" - return a + b - - -def multiply_numbers(a: int, b: int) -> int: - """Multiply two numbers together.""" - return a * b - - -# Create toolkit with debug enabled -calculator_tools = Toolkit( - name="calculator", - tools=[add_numbers, multiply_numbers], - instructions="Use these tools for basic math operations", - debug=True, -) - - -# Register additional function -def divide_numbers(a: int, b: int) -> float: - """Divide a by b.""" - return a / b - - -calculator_tools.register(divide_numbers) - -# Optionally, for demonstration, call list_functions and execute_function in debug mode -if __name__ == "__main__": - calculator_tools.list_functions() - result = calculator_tools.execute_function("add_numbers", a=5, b=3) - print(result) # This print is for script run, not for import diff --git a/isek/tools/fastmcp_toolkit.py b/isek/tools/fastmcp_toolkit.py deleted file mode 100644 index 401b506..0000000 --- a/isek/tools/fastmcp_toolkit.py +++ /dev/null @@ -1,244 +0,0 @@ -# isek/tools/fastmcp_toolkit.py - -import asyncio -from typing import Any, Dict, List, Optional -from fastmcp import Client -from isek.tools.toolkit import Toolkit -from isek.utils.log import log - - -class FastMCPToolkit(Toolkit): - """ - FastMCP-based toolkit for MCP server integration. - Provides simplified access to MCP tools with automatic discovery and registration. - """ - - def __init__( - self, - server_source: str, - name: str = "fastmcp", - timeout: float = 30.0, - auto_register: bool = True, - debug: bool = False, - auth_token: Optional[str] = None, - ): - """Initialize FastMCP toolkit. - - Args: - server_source: MCP server source (URL, file path, or FastMCP instance) - name: Toolkit name - timeout: Connection timeout in seconds - auto_register: Whether to automatically discover and register tools - debug: Enable debug output - auth_token: Authentication token for the MCP server - """ - self.server_source = server_source - self.timeout = timeout - self.auth_token = auth_token - self.client = None - - super().__init__( - name=name, - tools=[], - auto_register=False, - debug=debug, - ) - - if auto_register: - # Initialize connection and discover tools - self._initialize_connection() - - def _initialize_connection(self): - """Initialize MCP connection and discover tools.""" - try: - # Create FastMCP client - client_kwargs = { - "transport": self.server_source, - "timeout": self.timeout, - } - - if self.auth_token: - client_kwargs["auth"] = self.auth_token - - self.client = Client(**client_kwargs) - - # Discover and register tools - self._discover_tools() - - except Exception as e: - log.error(f"[FastMCPToolkit] Failed to initialize connection: {e}") - if self.debug: - raise - - def _discover_tools(self): - """Discover tools from MCP server and register them.""" - if not self.client: - return - - try: - # Get tools list - tools = self._run_async(self.client.list_tools()) - - if self.debug: - log.debug(f"[FastMCPToolkit] Discovered {len(tools)} tools") - - # Register each tool - for tool in tools: - self._register_mcp_tool(tool.name) - - except Exception as e: - log.error(f"[FastMCPToolkit] Tool discovery failed: {e}") - - def _register_mcp_tool(self, tool_name: str): - """Register a single MCP tool as a local function. - - Args: - tool_name: Name of the MCP tool to register - """ - - def tool_wrapper(**kwargs: Any) -> Any: - """Wrapper function that calls MCP tool.""" - try: - # Parameter mapping for common tools - mapped_kwargs = self._map_parameters(tool_name, kwargs) - - # Call the tool - result = self._run_async( - self.client.call_tool(tool_name, mapped_kwargs) - ) - - return self._extract_text(result) - - except Exception as e: - log.error(f"[FastMCPToolkit] Tool call failed for {tool_name}: {e}") - return f"[Error] {e}" - - # Set function attributes - tool_wrapper.__name__ = tool_name.replace(".", "_") - tool_wrapper.__doc__ = f"MCP tool: {tool_name}" - - # Register with toolkit - self.register(tool_wrapper, name=tool_wrapper.__name__) - - if self.debug: - log.debug(f"[FastMCPToolkit] Registered tool: {tool_name}") - - def _map_parameters(self, tool_name: str, kwargs: Dict[str, Any]) -> Dict[str, Any]: - """Map common parameter names to MCP tool specific parameter names. - - Args: - tool_name: Name of the MCP tool - kwargs: Original parameters - - Returns: - Mapped parameters - """ - mapped = kwargs.copy() - - # GitHub Copilot MCP parameter mappings - if tool_name == "search_repositories": - # Map 'q' to 'query' for GitHub search - if "q" in mapped and "query" not in mapped: - mapped["query"] = mapped.pop("q") - - return mapped - - def _run_async(self, coro): - """Run async coroutine in new event loop.""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - try: - - async def run_with_client(): - async with self.client: - return await coro - - result = loop.run_until_complete(run_with_client()) - return result - finally: - loop.close() - - def _extract_text(self, result) -> str: - """Extract text content from MCP result.""" - if not result or len(result) == 0: - return str(result) - - first_result = result[0] - - # Try to get text content safely - try: - if hasattr(first_result, "text") and first_result.text is not None: - return first_result.text - except (AttributeError, TypeError): - pass - - return str(first_result) - - def health_check(self) -> bool: - """Check if MCP connection is healthy. - - Returns: - True if connection is working, False otherwise - """ - if not self.client: - return False - - try: - self._run_async(self.client.ping()) - return True - except Exception as e: - log.error(f"[FastMCPToolkit] Health check failed: {e}") - return False - - def list_available_tools(self) -> List[str]: - """Get list of available MCP tools. - - Returns: - List of tool names - """ - if not self.client: - return [] - - try: - tools = self._run_async(self.client.list_tools()) - return [tool.name for tool in tools] - except Exception as e: - log.error(f"[FastMCPToolkit] Failed to get tools list: {e}") - return [] - - def call_tool(self, tool_name: str, **kwargs) -> Any: - """Call a specific MCP tool. - - Args: - tool_name: Name of the tool to call - **kwargs: Arguments to pass to the tool - - Returns: - Tool execution result - """ - if not self.client: - return "[Error] No MCP client available" - - try: - # Parameter mapping for common tools - mapped_kwargs = self._map_parameters(tool_name, kwargs) - - # Call the tool - result = self._run_async(self.client.call_tool(tool_name, mapped_kwargs)) - - return self._extract_text(result) - - except Exception as e: - log.error(f"[FastMCPToolkit] Tool call failed for {tool_name}: {e}") - return f"[Error] {e}" - - -# Convenience function -def create_fastmcp_toolkit( - server_source: str, auth_token: Optional[str] = None, debug: bool = False -) -> FastMCPToolkit: - """Create a FastMCP toolkit instance.""" - return FastMCPToolkit( - server_source=server_source, auth_token=auth_token, debug=debug - ) diff --git a/isek/tools/finance_toolkit/get_company_base_info.py b/isek/tools/finance_toolkit/get_company_base_info.py deleted file mode 100644 index 29abc78..0000000 --- a/isek/tools/finance_toolkit/get_company_base_info.py +++ /dev/null @@ -1,48 +0,0 @@ -from isek.tools.toolkit import Toolkit -import efinance as ef - -import requests -from bs4 import BeautifulSoup - - -def get_stock_info(stock_code: str): - """get base info of stock base on stock code""" - - return ef.stock.get_base_info(stock_code) - - -def get_company_info(url: str): - """fetch additional company details base this url""" - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" - } - - try: - response = requests.get(url, headers=headers) - response.encoding = "utf-8" - soup = BeautifulSoup(response.text, "html.parser") - return soup.get_text(strip=True) - except Exception: - return "" - - -# Create toolkit with debug enabled -company_base_info_tools = Toolkit( - name="stock_info_tool", - tools=[get_stock_info, get_company_info], - instructions="Fetch company information based on its stock code and the company's 'About' page or official website URL.", - debug=True, -) - - -# Optionally, for demonstration, call list_functions and execute_function in debug mode -if __name__ == "__main__": - company_base_info_tools.list_functions() - stock_info = company_base_info_tools.execute_function( - "get_stock_info", stock_code="00020" - ) - company_info = company_base_info_tools.execute_function( - "get_company_info", url="https://www.sensetime.com/cn/about-index#1" - ) - print("Stock Info:", stock_info) - print("Company Info:", company_info) diff --git a/isek/tools/toolkit.py b/isek/tools/toolkit.py deleted file mode 100644 index 680e471..0000000 --- a/isek/tools/toolkit.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional -from isek.utils.log import log -from isek.utils.tools import function_to_schema - - -@dataclass -class SimpleFunction: - """Ultra-simplified function wrapper.""" - - name: str - entrypoint: Callable - description: Optional[str] = None - parameters: Optional[Dict[str, Any]] = None - - def __post_init__(self): - if self.parameters is None: - self.parameters = {"type": "object", "properties": {}, "required": []} - - def to_dict(self) -> Dict[str, Any]: - """Convert function to dictionary.""" - return { - "name": self.name, - "description": self.description, - "parameters": self.parameters, - } - - def execute(self, **kwargs) -> Any: - """Execute the function with given arguments.""" - return self.entrypoint(**kwargs) - - -class Toolkit: - """Ultra-simplified Toolkit class with minimal features.""" - - def __init__( - self, - name: str = "toolkit", - tools: Optional[List[Callable]] = None, - instructions: Optional[str] = None, - auto_register: bool = True, - debug: bool = False, - ): - """Initialize a new Toolkit. - - Args: - name: A descriptive name for the toolkit - tools: List of tools to include in the toolkit - instructions: Instructions for the toolkit - auto_register: Whether to automatically register all tools - debug: Enable debug output - """ - self.name: str = name - self.tools: List[Callable] = tools or [] - self.functions: Dict[str, SimpleFunction] = {} - self.instructions: Optional[str] = instructions - self.debug: bool = debug - - # Automatically register all tools if auto_register is True - if auto_register and self.tools: - self._register_tools() - - def _register_tools(self) -> None: - """Register all tools.""" - for tool in self.tools: - self.register(tool) - - def register( - self, function: Callable, name: Optional[str] = None - ) -> SimpleFunction: - """Register a function with the toolkit. - - Args: - function: The callable to register - name: Optional custom name for the function - - Returns: - The registered SimpleFunction - """ - tool_name = name or function.__name__ - schema = function_to_schema(function) - parameters = schema["function"][ - "parameters" - ] # Extract just the parameters schema - simple_function = SimpleFunction( - name=tool_name, - entrypoint=function, - description=getattr(function, "__doc__", None), - parameters=parameters, - ) - self.functions[tool_name] = simple_function - if self.debug: - log.debug(f"[Toolkit: {self.name}] Registered function: {tool_name}") - return simple_function - - def get_function(self, name: str) -> Optional[SimpleFunction]: - """Get a function by name.""" - return self.functions.get(name) - - def list_functions(self) -> List[str]: - """List all registered function names.""" - if self.debug: - log.debug(f"[Toolkit: {self.name}] Listing functions:") - for fname in self.functions: - log.debug(f" - {fname}") - return list(self.functions.keys()) - - def execute_function(self, name: str, **kwargs) -> Any: - """Execute a function by name.""" - function = self.get_function(name) - if function is None: - raise ValueError(f"Function '{name}' not found in toolkit '{self.name}'") - result = function.execute(**kwargs) - if self.debug: - log.debug( - f"[Toolkit: {self.name}] Executed '{name}' with args {kwargs} -> {result}" - ) - return result - - def __repr__(self) -> str: - return f"<{self.__class__.__name__} name={self.name} functions={list(self.functions.keys())}>" - - def __str__(self) -> str: - return self.__repr__() diff --git a/pyproject.toml b/pyproject.toml index 6232537..c10127a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,35 +15,22 @@ requires-python = ">=3.10" keywords = ["LLM", "multi-agent", "distributed", "AI"] dependencies = [ + # Core framework dependencies "click>=8.0.0", - "loguru>=0.6.0", - "pyyaml>=6.0", - "requests>=2.28.0", - # Core dependencies - "openai>=0.27.0", - "protobuf==5.29.5", - "flask>=2.0.0", - "ecdsa", - "pydantic_ai", - "numpy>=1.23", - "python-dotenv", "httpx>=0.25.0", - "web3>=6.0.0", - "eth-account>=0.9.0", - "sphinx", - "sphinxawesome-theme", - "pytest", - "pre-commit", - "etcd3gw", - "mypy", - "litellm", "uvicorn", "rich", - "fastmcp", + "python-dotenv", + + # A2A protocol dependencies "a2a-sdk", - "aiortc", + + # AI/ML dependencies "pydantic-ai", - "httpx", + + # Web3/Ethereum dependencies + "web3>=6.0.0", + "eth-account>=0.9.0", ] [project.scripts] diff --git a/scripts/delete_all_etcd_node.py b/scripts/delete_all_etcd_node.py deleted file mode 100644 index f605d93..0000000 --- a/scripts/delete_all_etcd_node.py +++ /dev/null @@ -1,7 +0,0 @@ -from isek.node.etcd_registry import EtcdRegistry - - -registry = EtcdRegistry(host="47.236.116.81", port=2379) -nodes = registry.get_available_nodes() -for key, value in nodes.items(): - registry.deregister_node(key) diff --git a/tests/etcd_registry_test.py b/tests/etcd_registry_test.py deleted file mode 100644 index ef54380..0000000 --- a/tests/etcd_registry_test.py +++ /dev/null @@ -1,26 +0,0 @@ -# import ecdsa -# from isek.node.etcd_registry import EtcdRegistry - - -# def test_etcd_registry(): -# node_a = EtcdRegistry(host="47.236.116.81", port=2379) -# node_a.register_node("node_a", "localhost", 8080, {"name": "a"}) - -# node_b = EtcdRegistry(host="47.236.116.81", port=2379) -# node_b.register_node("node_b", "localhost", 8082, {"name": "b"}) - -# node_c = EtcdRegistry(host="47.236.116.81", port=2379, parent_node_id="abc") -# node_c.register_node("node_c", "localhost", 8082, {"name": "c"}) - -# assert len(node_b.get_available_nodes()) == 2 - -# try: -# node_b.deregister_node("node_a") -# except ValueError as e: -# print(f"node_b can not deregister node_a: {e}") -# assert len(node_b.get_available_nodes()) == 2 -# node_a.deregister_node("node_a") -# print("node_a deregister node_a") -# assert len(node_b.get_available_nodes()) == 1 -# assert len(node_c.get_available_nodes()) == 1 - diff --git a/tests/isek_center_registry_test.py b/tests/isek_center_registry_test.py deleted file mode 100644 index d3c5cf3..0000000 --- a/tests/isek_center_registry_test.py +++ /dev/null @@ -1,24 +0,0 @@ -# import time - -# from isek.node.isek_center_registry import IsekCenterRegistry -# from isek import isek_center -# import threading - - -# def test_isek_center_registry(): -# server_thread = threading.Thread(target=isek_center.main, daemon=True) -# server_thread.start() -# time.sleep(2) - -# node_a = IsekCenterRegistry() -# node_a.register_node("node_a", "localhost", 8080, {"name": "a"}) - -# node_b = IsekCenterRegistry() -# node_b.register_node("node_b", "localhost", 8082, {"name": "b"}) - -# all_nodes = node_b.get_available_nodes() -# assert len(all_nodes) == 2 -# node_a.deregister_node("node_a") -# all_nodes = node_b.get_available_nodes() -# assert len(all_nodes) == 1 -# node_b.deregister_node("node_b") diff --git a/tests/log_test.py b/tests/log_test.py deleted file mode 100644 index 36a5c5a..0000000 --- a/tests/log_test.py +++ /dev/null @@ -1,51 +0,0 @@ -from isek.utils.log import LoggerManager, log, team_log - - -def test_loggers_exist(): - """Test that the default loggers can be imported and used.""" - assert log is not None - assert team_log is not None - log.info("Test agent logger.") - team_log.info("Test team logger.") - - -def test_set_log_level(capsys): - """Test setting the log level.""" - # Set level to DEBUG and check if debug message is printed - LoggerManager.set_level("DEBUG", name="agent") - log.debug("This is a debug message.") - captured = capsys.readouterr() - assert "This is a debug message." in captured.out - - # Set level to INFO and check if debug message is NOT printed - LoggerManager.set_level("INFO", name="agent") - log.debug("This should not be printed.") - captured = capsys.readouterr() - assert "This should not be printed." not in captured.out - - log.info("This should be printed.") - captured = capsys.readouterr() - assert "This should be printed." in captured.out - - -def test_print_method(capsys): - """Test the custom .print() method.""" - message = "[bold green]This is a rich test message![/bold green]" - log.print(message) - captured = capsys.readouterr() - # Rich adds its own formatting, so we check for the core text - assert "This is a rich test message!" in captured.out - - -def test_team_logger_separate(capsys): - """Test that agent and team loggers can have separate levels.""" - LoggerManager.set_level("INFO", name="agent") - LoggerManager.set_level("DEBUG", name="team") - - log.debug("Agent debug should not appear.") - captured = capsys.readouterr() - assert "Agent debug should not appear." not in captured.out - - team_log.debug("Team debug should appear.") - captured = capsys.readouterr() - assert "Team debug should appear." in captured.out diff --git a/tests/node_test.py b/tests/node_test.py deleted file mode 100644 index c7bf103..0000000 --- a/tests/node_test.py +++ /dev/null @@ -1,31 +0,0 @@ -import time -from isek.node.node_v2 import Node -from isek.node.etcd_registry import EtcdRegistry -from isek.adapter.simple_adapter import SimpleAdapter - - -def test_node_message(): - registry = EtcdRegistry(host="47.236.116.81", port=2379) - - # Create teams for the nodes - team1 = SimpleAdapter(name="Node1Team", description="Team for Node1") - team2 = SimpleAdapter(name="Node2Team", description="Team for Node2") - - node1 = Node(node_id="Node1", registry=registry, port=8080, team=team1) - node2 = Node(node_id="Node2", registry=registry, port=8081, team=team2) - - node1.build_server(daemon=True) - node2.build_server(daemon=True) - time.sleep(5) - result = node1.send_message(node2.node_id, "Hello, I am Node1") - print(result) - - -def build_node(): - # Create a simple team for the node - team = SimpleAdapter(name="TestTeam", description="A test team for node testing") - Node(team=team).build_server() - - -if __name__ == "__main__": - build_node() diff --git a/tests/test_fastmcp.py b/tests/test_fastmcp.py deleted file mode 100644 index d3c437d..0000000 --- a/tests/test_fastmcp.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script for FastMCP toolkit -""" - -import os -import sys -from pathlib import Path - -# Add project root to path -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root)) - -# Import after path setup -from isek.tools.fastmcp_toolkit import fastmcp_tools # noqa: E402 -from isek.utils.log import log # noqa: E402 - - -def test_fastmcp_basic(): - """Test basic FastMCP functionality""" - print("=== FastMCP Basic Test ===") - - # Test with GitHub Copilot MCP server - github_token = os.getenv("GITHUB_TOKEN") - - if not github_token: - print("❌ GITHUB_TOKEN not set") - print("Please set GITHUB_TOKEN environment variable") - print("Example: export GITHUB_TOKEN='your_github_token'") - return False - - try: - print("🔧 Using pre-created FastMCP toolkit...") - toolkit = fastmcp_tools - - print("🔍 Checking connection health...") - health = toolkit.health_check() - print(f"Health check result: {health}") - - if health: - print("✅ Connection successful!") - - print("📋 Listing available tools...") - tools = toolkit.list_available_tools() - print(f"Found {len(tools)} tools:") - for i, tool in enumerate(tools[:5], 1): # Show first 5 tools - print(f" {i}. {tool}") - - if len(tools) > 5: - print(f" ... and {len(tools) - 5} more") - - print("\n📝 Listing registered functions...") - functions = toolkit.list_functions() - print(f"Registered functions: {functions}") - - return True - else: - print("❌ Connection failed") - return False - - except Exception as e: - print(f"❌ Error: {e}") - log.error(f"FastMCP test failed: {e}") - return False - - -def test_fastmcp_tool_call(): - """Test MCP tool calling""" - print("\n=== FastMCP Tool Call Test ===") - - github_token = os.getenv("GITHUB_TOKEN") - if not github_token: - print("❌ GITHUB_TOKEN not set, skipping tool call test") - return False - - try: - toolkit = fastmcp_tools - - # Get available tools - tools = toolkit.list_available_tools() - if not tools: - print("❌ No tools available") - return False - - # Try to call the first available tool - first_tool = tools[0] - print(f"🔧 Testing tool call: {first_tool}") - - result = toolkit.call_tool(first_tool, test="hello") - print(f"Tool call result: {result}") - - return True - - except Exception as e: - print(f"❌ Tool call test failed: {e}") - return False - - -def test_github_search_repositories(): - """Test GitHub search_repositories with correct parameters""" - print("\n=== GitHub Search Repositories Test ===") - - github_token = os.getenv("GITHUB_TOKEN") - if not github_token: - print("❌ GITHUB_TOKEN not set, skipping GitHub test") - return False - - try: - toolkit = fastmcp_tools - - # Test with correct parameter name 'query' - print("🔧 Testing search_repositories with 'query' parameter...") - result = toolkit.call_tool( - "search_repositories", query="Python machine learning" - ) - print(f"Result with 'query': {result}") - - # Test with incorrect parameter name 'q' - print("\n🔧 Testing search_repositories with 'q' parameter...") - result = toolkit.call_tool("search_repositories", q="Python machine learning") - print(f"Result with 'q': {result}") - - return True - - except Exception as e: - print(f"❌ GitHub search test failed: {e}") - return False - - -def main(): - """Main test function""" - print("🚀 Starting FastMCP Toolkit Tests") - print("=" * 50) - - # Test basic functionality - basic_success = test_fastmcp_basic() - - # Test tool calling - tool_call_success = test_fastmcp_tool_call() - - # Test GitHub search repositories - github_success = test_github_search_repositories() - - # Summary - print("\n" + "=" * 50) - print("📊 Test Results:") - print(f" Basic functionality: {'✅ PASS' if basic_success else '❌ FAIL'}") - print(f" Tool calling: {'✅ PASS' if tool_call_success else '❌ FAIL'}") - print(f" GitHub search: {'✅ PASS' if github_success else '❌ FAIL'}") - - if basic_success and tool_call_success and github_success: - print("\n🎉 All tests passed!") - return 0 - else: - print("\n⚠️ Some tests failed") - return 1 - - -if __name__ == "__main__": - exit(main()) diff --git a/tests/test_memory.py b/tests/test_memory.py deleted file mode 100644 index 44e7c67..0000000 --- a/tests/test_memory.py +++ /dev/null @@ -1,195 +0,0 @@ -import pytest -from datetime import datetime -from isek.memory.memory import Memory, UserMemory, SessionSummary - - -@pytest.fixture -def memory(): - return Memory(debug_mode=True) - - -@pytest.fixture -def sample_user_memory(): - return UserMemory( - memory="This is a test memory", - topics=["test", "example"], - last_updated=datetime.now(), - ) - - -@pytest.fixture -def sample_session_summary(): - return SessionSummary( - summary="This is a test session summary", - topics=["test", "session"], - last_updated=datetime.now(), - ) - - -def test_add_user_memory(memory, sample_user_memory): - """Test adding and retrieving user memories""" - # Add memory - memory_id = memory.add_user_memory(sample_user_memory, user_id="test_user") - assert memory_id is not None - - # Retrieve memory - retrieved_memory = memory.get_user_memory(memory_id, user_id="test_user") - assert retrieved_memory is not None - assert retrieved_memory.memory == "This is a test memory" - assert "test" in retrieved_memory.topics - - -def test_get_user_memories(memory, sample_user_memory): - """Test getting all memories for a user""" - # Add multiple memories - memory.add_user_memory(sample_user_memory, user_id="test_user") - - second_memory = UserMemory(memory="Another test memory", topics=["another", "test"]) - memory.add_user_memory(second_memory, user_id="test_user") - - # Get all memories - memories = memory.get_user_memories(user_id="test_user") - assert len(memories) == 2 - assert any(m.memory == "This is a test memory" for m in memories) - assert any(m.memory == "Another test memory" for m in memories) - - -def test_delete_user_memory(memory, sample_user_memory): - """Test deleting user memories""" - # Add memory - memory_id = memory.add_user_memory(sample_user_memory, user_id="test_user") - - # Verify it exists - assert memory.get_user_memory(memory_id, user_id="test_user") is not None - - # Delete memory - result = memory.delete_user_memory(memory_id, user_id="test_user") - assert result is True - - # Verify it's deleted - assert memory.get_user_memory(memory_id, user_id="test_user") is None - - -def test_session_summaries(memory, sample_session_summary): - """Test session summary operations""" - # Add session summary - session_id = memory.add_session_summary( - session_id="test_session", summary=sample_session_summary, user_id="test_user" - ) - assert session_id == "test_session" - - # Retrieve session summary - retrieved_summary = memory.get_session_summary("test_session", user_id="test_user") - assert retrieved_summary is not None - assert retrieved_summary.summary == "This is a test session summary" - assert "test" in retrieved_summary.topics - - -def test_runs(memory): - """Test run operations""" - session_id = "test_session" - - # Add runs - memory.add_run(session_id, {"action": "test_action", "result": "success"}) - memory.add_run(session_id, {"action": "another_action", "result": "success"}) - - # Get runs - runs = memory.get_runs(session_id) - assert len(runs) == 2 - assert runs[0]["action"] == "test_action" - assert runs[1]["action"] == "another_action" - - -def test_clear_memory(memory, sample_user_memory, sample_session_summary): - """Test clearing all memory""" - # Add some data - memory.add_user_memory(sample_user_memory, user_id="test_user") - memory.add_session_summary( - "test_session", sample_session_summary, user_id="test_user" - ) - memory.add_run("test_session", {"action": "test"}) - - # Verify data exists - assert len(memory.get_user_memories(user_id="test_user")) > 0 - assert memory.get_session_summary("test_session", user_id="test_user") is not None - assert len(memory.get_runs("test_session")) > 0 - - # Clear memory - memory.clear() - - # Verify data is cleared - assert len(memory.get_user_memories(user_id="test_user")) == 0 - assert memory.get_session_summary("test_session", user_id="test_user") is None - assert len(memory.get_runs("test_session")) == 0 - - -def test_memory_to_dict(memory, sample_user_memory, sample_session_summary): - """Test converting memory to dictionary""" - # Add some data - memory.add_user_memory(sample_user_memory, user_id="test_user") - memory.add_session_summary( - "test_session", sample_session_summary, user_id="test_user" - ) - memory.add_run("test_session", {"action": "test"}) - - # Convert to dict - memory_dict = memory.to_dict() - - # Verify structure - assert "memories" in memory_dict - assert "summaries" in memory_dict - assert "runs" in memory_dict - assert "test_user" in memory_dict["memories"] - assert "test_user" in memory_dict["summaries"] - assert "test_session" in memory_dict["runs"] - - -def test_user_memory_to_dict(sample_user_memory): - """Test UserMemory to_dict method""" - memory_dict = sample_user_memory.to_dict() - - assert "memory" in memory_dict - assert "topics" in memory_dict - assert "last_updated" in memory_dict - assert memory_dict["memory"] == "This is a test memory" - assert "test" in memory_dict["topics"] - - -def test_session_summary_to_dict(sample_session_summary): - """Test SessionSummary to_dict method""" - summary_dict = sample_session_summary.to_dict() - - assert "session_id" in summary_dict - assert "summary" in summary_dict - assert "topics" in summary_dict - assert "last_updated" in summary_dict - assert summary_dict["summary"] == "This is a test session summary" - assert "test" in summary_dict["topics"] - - -def test_memory_repr(memory): - """Test memory string representation""" - repr_str = repr(memory) - assert "Memory" in repr_str - assert "users=0" in repr_str - assert "sessions=0" in repr_str - assert "runs=0" in repr_str - - -def test_multiple_users(memory, sample_user_memory): - """Test memory isolation between users""" - # Add memory for user1 - memory.add_user_memory(sample_user_memory, user_id="user1") - - # Add different memory for user2 - user2_memory = UserMemory(memory="User 2 memory", topics=["user2", "test"]) - memory.add_user_memory(user2_memory, user_id="user2") - - # Verify isolation - user1_memories = memory.get_user_memories(user_id="user1") - user2_memories = memory.get_user_memories(user_id="user2") - - assert len(user1_memories) == 1 - assert len(user2_memories) == 1 - assert user1_memories[0].memory == "This is a test memory" - assert user2_memories[0].memory == "User 2 memory" diff --git a/tests/test_openai_simple.py b/tests/test_openai_simple.py deleted file mode 100644 index 2ba9dd7..0000000 --- a/tests/test_openai_simple.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -"""Super simple test for OpenAIModel.""" - -import os -from isek.models.openai import OpenAIModel -from isek.models.base import SimpleMessage -import dotenv - -dotenv.load_dotenv() - - -def test_openai_model(): - """Test the OpenAIModel implementation.""" - - if not os.environ.get("OPENAI_API_KEY"): - print("OPENAI_API_KEY not set, skipping test") - return - - try: - model = OpenAIModel(model_id="gpt-3.5-turbo") - messages = [SimpleMessage(role="user", content="Say 'Hello from OpenAI!'")] - response = model.response(messages) - print(f"Response: {response.content}") - - except Exception as e: - print(f"Test failed: {e}") - - -if __name__ == "__main__": - test_openai_model() diff --git a/tests/test_toolkit.py b/tests/test_toolkit.py deleted file mode 100644 index b4ad92b..0000000 --- a/tests/test_toolkit.py +++ /dev/null @@ -1,275 +0,0 @@ -import pytest -from isek.tools.toolkit import Toolkit, SimpleFunction -from typing import Optional - - -@pytest.fixture -def toolkit(): - return Toolkit(name="TestToolkit", debug=True) - - -@pytest.fixture -def sample_functions(): - """Sample functions for testing""" - - def add(a: int, b: int) -> int: - """Add two numbers together.""" - return a + b - - def greet(name: str, greeting: str = "Hello") -> str: - """Greet someone with a custom message.""" - return f"{greeting}, {name}!" - - def multiply(x: float, y: float) -> float: - """Multiply two numbers.""" - return x * y - - return [add, greet, multiply] - - -def test_toolkit_initialization(): - """Test toolkit initialization with different parameters""" - # Basic initialization - toolkit = Toolkit() - assert toolkit.name == "toolkit" - assert len(toolkit.functions) == 0 - assert toolkit.debug is False - - # Custom initialization - toolkit = Toolkit(name="CustomToolkit", debug=True) - assert toolkit.name == "CustomToolkit" - assert toolkit.debug is True - - -def test_register_function(toolkit): - """Test registering individual functions""" - - def test_function(x: int) -> int: - """Test function that doubles a number.""" - return x * 2 - - # Register function - simple_func = toolkit.register(test_function) - - # Check registration - assert "test_function" in toolkit.functions - assert toolkit.get_function("test_function") is not None - assert simple_func.name == "test_function" - assert simple_func.description == "Test function that doubles a number." - - # Test execution - result = toolkit.execute_function("test_function", x=5) - assert result == 10 - - -def test_register_with_custom_name(toolkit): - """Test registering function with custom name""" - - def original_function(): - return "test" - - # Register with custom name - toolkit.register(original_function, name="custom_name") - - # Check registration - assert "custom_name" in toolkit.functions - assert "original_function" not in toolkit.functions - - -def test_auto_register_tools(sample_functions): - """Test automatic registration of tools during initialization""" - toolkit = Toolkit( - name="AutoToolkit", tools=sample_functions, auto_register=True, debug=True - ) - - # Check that all functions were registered - assert len(toolkit.functions) == 3 - assert "add" in toolkit.functions - assert "greet" in toolkit.functions - assert "multiply" in toolkit.functions - - -def test_no_auto_register(): - """Test toolkit without auto-registration""" - - def test_func(): - return "test" - - toolkit = Toolkit(name="ManualToolkit", tools=[test_func], auto_register=False) - - # Check that no functions were registered - assert len(toolkit.functions) == 0 - - -def test_execute_function(toolkit): - """Test function execution""" - - def add_numbers(a: int, b: int, c: int = 0) -> int: - """Add three numbers with optional third parameter.""" - return a + b + c - - toolkit.register(add_numbers) - - # Test basic execution - result = toolkit.execute_function("add_numbers", a=1, b=2) - assert result == 3 - - # Test with optional parameter - result = toolkit.execute_function("add_numbers", a=1, b=2, c=3) - assert result == 6 - - -def test_execute_nonexistent_function(toolkit): - """Test error handling for non-existent function""" - with pytest.raises(ValueError, match="Function 'nonexistent' not found"): - toolkit.execute_function("nonexistent", x=1) - - -def test_list_functions(toolkit, sample_functions): - """Test listing registered functions""" - # Register functions - for func in sample_functions: - toolkit.register(func) - - # Get function list - function_names = toolkit.list_functions() - - # Check results - assert len(function_names) == 3 - assert "add" in function_names - assert "greet" in function_names - assert "multiply" in function_names - - -def test_get_function(toolkit): - """Test getting function by name""" - - def test_func(): - return "test" - - toolkit.register(test_func) - - # Get function - func = toolkit.get_function("test_func") - assert func is not None - assert func.name == "test_func" - assert func.entrypoint == test_func - - # Get non-existent function - func = toolkit.get_function("nonexistent") - assert func is None - - -def test_simple_function_execution(): - """Test SimpleFunction execution directly""" - - def test_func(x: int, y: str = "default") -> str: - return f"{y}: {x}" - - simple_func = SimpleFunction( - name="test", entrypoint=test_func, description="Test function" - ) - - # Test execution - result = simple_func.execute(x=5, y="Value") - assert result == "Value: 5" - - # Test with default parameter - result = simple_func.execute(x=10) - assert result == "default: 10" - - -def test_simple_function_to_dict(): - """Test SimpleFunction to_dict method""" - - def test_func(x: int) -> int: - """Test function.""" - return x * 2 - - simple_func = SimpleFunction( - name="test_func", - entrypoint=test_func, - description="Test function.", - parameters={ - "type": "object", - "properties": {"x": {"type": "integer"}}, - "required": ["x"], - }, - ) - - func_dict = simple_func.to_dict() - - assert func_dict["name"] == "test_func" - assert func_dict["description"] == "Test function." - assert "parameters" in func_dict - assert func_dict["parameters"]["type"] == "object" - - -def test_toolkit_repr(toolkit): - """Test toolkit string representation""" - - def test_func(): - pass - - toolkit.register(test_func) - - repr_str = repr(toolkit) - assert "Toolkit" in repr_str - assert "TestToolkit" in repr_str - assert "test_func" in repr_str - - -def test_toolkit_with_instructions(): - """Test toolkit with instructions""" - instructions = "This toolkit provides mathematical operations." - toolkit = Toolkit(name="MathToolkit", instructions=instructions) - - assert toolkit.instructions == instructions - - -def test_function_parameter_schema(toolkit): - """Test that function parameter schemas are properly generated""" - - def complex_function( - name: str, age: int, active: bool = True, scores: Optional[list] = None - ) -> str: - """A function with various parameter types.""" - return f"{name} is {age} years old" - - toolkit.register(complex_function) - - func = toolkit.get_function("complex_function") - assert func is not None - - # Check that parameters schema exists - assert func.parameters is not None - assert "properties" in func.parameters - - # Test execution - result = toolkit.execute_function("complex_function", name="Alice", age=30) - assert result == "Alice is 30 years old" - - -def test_multiple_toolkits(): - """Test that multiple toolkits can coexist independently""" - toolkit1 = Toolkit(name="Toolkit1") - toolkit2 = Toolkit(name="Toolkit2") - - def func1(): - return "toolkit1" - - def func2(): - return "toolkit2" - - toolkit1.register(func1) - toolkit2.register(func2) - - # Check independence - assert len(toolkit1.functions) == 1 - assert len(toolkit2.functions) == 1 - assert "func1" in toolkit1.functions - assert "func2" in toolkit2.functions - - # Test execution - assert toolkit1.execute_function("func1") == "toolkit1" - assert toolkit2.execute_function("func2") == "toolkit2" From 40daa9d6524289675d6ff72af37b8af3c8731483 Mon Sep 17 00:00:00 2001 From: Moshi Wei Date: Sat, 11 Oct 2025 23:03:28 +0800 Subject: [PATCH 2/2] Update pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c10127a..3411b15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "uvicorn", "rich", "python-dotenv", + "pre-commit", # A2A protocol dependencies "a2a-sdk",