Paper2Agent commited on
Commit
8b54db1
·
verified ·
1 Parent(s): 4d1229b

Upload 56 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. AlphaGenome_mcp.py +74 -0
  2. README.md +6 -5
  3. __pycache__/agent_v3.cpython-311.pyc +0 -0
  4. agent_v3.py +587 -0
  5. app.py +649 -62
  6. core/__init__.py +8 -0
  7. core/__pycache__/__init__.cpython-311.pyc +0 -0
  8. core/__pycache__/__init__.cpython-312.pyc +0 -0
  9. core/__pycache__/constants.cpython-311.pyc +0 -0
  10. core/__pycache__/constants.cpython-312.pyc +0 -0
  11. core/__pycache__/types.cpython-311.pyc +0 -0
  12. core/__pycache__/types.cpython-312.pyc +0 -0
  13. core/constants.py +259 -0
  14. core/types.py +29 -0
  15. images.png +0 -0
  16. managers/.DS_Store +0 -0
  17. managers/__init__.py +41 -0
  18. managers/__pycache__/__init__.cpython-311.pyc +0 -0
  19. managers/execution/__init__.py +8 -0
  20. managers/execution/__pycache__/__init__.cpython-311.pyc +0 -0
  21. managers/execution/__pycache__/monitoring.cpython-311.pyc +0 -0
  22. managers/execution/__pycache__/python_executor.cpython-311.pyc +0 -0
  23. managers/execution/monitoring.py +52 -0
  24. managers/execution/python_executor.py +138 -0
  25. managers/support/__init__.py +8 -0
  26. managers/support/__pycache__/__init__.cpython-311.pyc +0 -0
  27. managers/support/__pycache__/console_display.cpython-311.pyc +0 -0
  28. managers/support/__pycache__/package_manager.cpython-311.pyc +0 -0
  29. managers/support/console_display.py +140 -0
  30. managers/support/package_manager.py +58 -0
  31. managers/tools/__init__.py +23 -0
  32. managers/tools/__pycache__/__init__.cpython-311.pyc +0 -0
  33. managers/tools/__pycache__/builtin_tools.cpython-311.pyc +0 -0
  34. managers/tools/__pycache__/mcp_manager.cpython-311.pyc +0 -0
  35. managers/tools/__pycache__/tool_manager.cpython-311.pyc +0 -0
  36. managers/tools/__pycache__/tool_registry.cpython-311.pyc +0 -0
  37. managers/tools/__pycache__/tool_selector.cpython-311.pyc +0 -0
  38. managers/tools/builtin_tools.py +34 -0
  39. managers/tools/mcp_manager.py +588 -0
  40. managers/tools/tool_manager.py +521 -0
  41. managers/tools/tool_registry.py +314 -0
  42. managers/tools/tool_selector.py +109 -0
  43. managers/workflow/__init__.py +9 -0
  44. managers/workflow/__pycache__/__init__.cpython-311.pyc +0 -0
  45. managers/workflow/__pycache__/plan_manager.cpython-311.pyc +0 -0
  46. managers/workflow/__pycache__/state_manager.cpython-311.pyc +0 -0
  47. managers/workflow/__pycache__/workflow_engine.cpython-311.pyc +0 -0
  48. managers/workflow/plan_manager.py +78 -0
  49. managers/workflow/state_manager.py +24 -0
  50. managers/workflow/workflow_engine.py +303 -0
AlphaGenome_mcp.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model Context Protocol (MCP) for AlphaGenome
3
+
4
+ AlphaGenome provides state-of-the-art AI-driven tools for genomic sequence analysis, variant effect prediction, and functional genomics visualization. The platform enables researchers to predict regulatory elements, assess variant impacts, and explore molecular mechanisms across diverse cell types and tissues.
5
+
6
+ This MCP Server contains the tools extracted from the following tutorials with their tools:
7
+ 1. batch_variant_scoring
8
+ - score_variants_batch: Score multiple genetic variants in batch using configurable variant scorers
9
+ - filter_variant_scores: Filter variant scores by ontology criteria (e.g., cell types)
10
+ 2. essential_commands
11
+ - create_genomic_interval: Create genomic intervals for DNA regions
12
+ - create_genomic_variant: Create genomic variants for genetic changes
13
+ - create_track_data: Create TrackData objects from arrays and metadata
14
+ - create_variant_scores: Create AnnData objects for variant scoring results
15
+ - genomic_interval_operations: Perform operations on genomic intervals
16
+ - variant_interval_operations: Check variant overlaps with intervals
17
+ - track_data_operations: Filter, resize, slice and transform TrackData
18
+ - track_data_resolution_conversion: Convert between track data resolutions
19
+ 3. example_analysis_workflow
20
+ # REMOVED - Too TAL1-specific:
21
+ # - visualize_tal1_variant_positions: Visualize genomic positions of oncogenic TAL1 variants
22
+ # - predict_variant_functional_impact: Predict functional effects of specific TAL1 variants
23
+ # - compare_oncogenic_vs_background_variants: Compare predicted effects of disease variants vs background
24
+ 4. quick_start
25
+ - predict_dna_sequence: Predict genomic tracks from DNA sequence
26
+ - predict_genome_interval: Predict genomic tracks for reference genome intervals
27
+ # REMOVED - Redundant with visualize_variant_effects:
28
+ # - predict_variant_effects: Predict and visualize genetic variant effects
29
+ # REMOVED - Redundant with score_variants_batch:
30
+ # - score_variant_effect: Score genetic variant effects using variant scorers
31
+ - ism_analysis: Perform in silico mutagenesis analysis with sequence logos
32
+ # REMOVED - Should be parameter in other functions:
33
+ # - mouse_predictions: Make predictions for mouse sequences and intervals
34
+ 5. tissue_ontology_mapping
35
+ - explore_output_metadata: Explore and filter output metadata for specific organisms and search terms
36
+ - count_tracks_by_output_type: Count tracks by output type for human and mouse organisms
37
+ 6. variant_scoring_ui
38
+ # REMOVED - Redundant with score_variants_batch:
39
+ # - score_variant: Score a single variant with multiple variant scorers and save results
40
+ - visualize_variant_effects: Generate comprehensive variant effect visualization across multiple modalities
41
+ 7. visualization_modality_tour
42
+ - visualize_gene_expression: Visualize RNA_SEQ and CAGE gene expression predictions
43
+ # REMOVED - Redundant with visualize_variant_effects:
44
+ # - visualize_variant_expression_effects: Show REF vs ALT variant effects on gene expression
45
+ # REMOVED - Too specific:
46
+ # - visualize_custom_annotations: Plot custom annotations like polyadenylation sites
47
+ - visualize_chromatin_accessibility: Visualize DNASE and ATAC chromatin accessibility
48
+ - visualize_splicing_effects: Visualize splicing predictions with SPLICE_SITES, SPLICE_SITE_USAGE, SPLICE_JUNCTIONS
49
+ # REMOVED - Redundant with visualize_variant_effects:
50
+ # - visualize_variant_splicing_effects: Show REF vs ALT variant effects on splicing with sashimi plots
51
+ - visualize_histone_modifications: Visualize CHIP_HISTONE predictions with custom colors
52
+ - visualize_tf_binding: Visualize CHIP_TF transcription factor binding predictions
53
+ - visualize_contact_maps: Visualize CONTACT_MAPS DNA-DNA contact predictions
54
+ """
55
+
56
+ import sys
57
+ from pathlib import Path
58
+ from fastmcp import FastMCP
59
+ import requests
60
+
61
+ # Import the MCP tools from the tools folder
62
+ from tools.batch_variant_scoring import batch_variant_scoring_mcp
63
+ from tools.essential_commands import essential_commands_mcp
64
+
65
+ # Define the MCP server
66
+ mcp = FastMCP(name = "AlphaGenome")
67
+
68
+ # Mount the tools
69
+ mcp.mount(batch_variant_scoring_mcp)
70
+ mcp.mount(essential_commands_mcp)
71
+
72
+ # Run the MCP server
73
+ if __name__ == "__main__":
74
+ mcp.run(show_banner=False)
README.md CHANGED
@@ -1,10 +1,11 @@
1
  ---
2
- title: Alphagenome Agent
3
- emoji: 🏆
4
- colorFrom: green
5
- colorTo: red
 
6
  sdk: gradio
7
- sdk_version: 5.44.1
8
  app_file: app.py
9
  pinned: false
10
  ---
 
1
  ---
2
+ title: Alpha
3
+ emoji: 👁
4
+ colorFrom: blue
5
+ python_version: 3.11
6
+ colorTo: gray
7
  sdk: gradio
8
+ sdk_version: 5.49.0
9
  app_file: app.py
10
  pinned: false
11
  ---
__pycache__/agent_v3.cpython-311.pyc ADDED
Binary file (28.6 kB). View file
 
agent_v3.py ADDED
@@ -0,0 +1,587 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeAgent: A LangGraph-based agent for executing Python code and using tools.
3
+ Fully modular version with unified tool management.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import time
9
+ from typing import Dict, List, Optional
10
+ from jinja2 import Template
11
+ from langchain_core.language_models.chat_models import BaseChatModel
12
+ from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
13
+ from langchain_openai import ChatOpenAI
14
+ from dotenv import load_dotenv
15
+
16
+ # Import core types and constants
17
+ from core.types import AgentState, AgentConfig
18
+ from core.constants import SYSTEM_PROMPT_TEMPLATE
19
+
20
+ # Import managers (organized by subsystem)
21
+ from managers import (
22
+ # Support
23
+ PackageManager,
24
+ ConsoleDisplay,
25
+ # Workflow
26
+ PlanManager,
27
+ StateManager,
28
+ WorkflowEngine,
29
+ # Tools
30
+ ToolManager,
31
+ ToolSource,
32
+ ToolSelector,
33
+ # Execution
34
+ Timing,
35
+ PythonExecutor
36
+ )
37
+
38
+ # Load environment variables
39
+ load_dotenv("./.env")
40
+
41
+
42
+ def get_system_prompt(functions: Dict[str, dict], packages: Dict[str, str] = None) -> str:
43
+ """Generate system prompt using template and functions."""
44
+ if packages is None:
45
+ from core.constants import LIBRARY_CONTENT_DICT
46
+ packages = LIBRARY_CONTENT_DICT
47
+ return Template(SYSTEM_PROMPT_TEMPLATE).render(functions=functions, packages=packages)
48
+
49
+
50
+ class CodeAgent:
51
+ """A code-based agent that can execute Python code and use tools to solve tasks."""
52
+
53
+ def __init__(self, model: BaseChatModel,
54
+ config: Optional[AgentConfig] = None,
55
+ use_tool_manager: bool = True,
56
+ use_tool_selection: bool = True):
57
+ """
58
+ Initialize the CodeAgent with unified tool management.
59
+
60
+ Args:
61
+ model: The language model to use for generation
62
+ config: Configuration for the agent
63
+ use_tool_manager: Whether to use the unified ToolManager (recommended)
64
+ use_tool_selection: Whether to use LLM-based tool selection (like Biomni)
65
+ """
66
+ self.model = model
67
+ self.config = config or AgentConfig()
68
+ self.use_tool_manager = use_tool_manager
69
+ self.use_tool_selection = use_tool_selection
70
+
71
+ # Cache selected tools to avoid re-selection at each step
72
+ self._selected_tools_cache = None
73
+
74
+ # Initialize modular components
75
+ self.package_manager = PackageManager()
76
+ self.console = ConsoleDisplay()
77
+ self.state_manager = StateManager()
78
+ self.plan_manager = PlanManager()
79
+
80
+ # Initialize unified tool management
81
+ if not self.use_tool_manager:
82
+ raise ValueError("ToolManager is required. Legacy mode (use_tool_manager=False) has been removed.")
83
+
84
+ self.tool_manager = ToolManager(self.console)
85
+
86
+ # Initialize tool selector for LLM-based tool selection
87
+ if self.use_tool_selection:
88
+ self.tool_selector = ToolSelector(self.model)
89
+ else:
90
+ self.tool_selector = None
91
+
92
+ # Initialize workflow engine
93
+ self.workflow_engine = WorkflowEngine(model, self.config, self.console, self.state_manager)
94
+
95
+ # Initialize Python executor
96
+ self.python_executor = PythonExecutor()
97
+
98
+ # Setup workflow
99
+ self._setup_workflow()
100
+
101
+ # ====================
102
+ # WORKFLOW SETUP
103
+ # ====================
104
+
105
+ def _setup_workflow(self):
106
+ """Setup the LangGraph workflow using WorkflowEngine."""
107
+ self.workflow_engine.setup_workflow(
108
+ self.generate,
109
+ self.execute,
110
+ self.should_continue
111
+ )
112
+
113
+ # ====================
114
+ # WORKFLOW NODES
115
+ # ====================
116
+
117
+ def generate(self, state: AgentState) -> AgentState:
118
+ """Generate response using LLM with tool-aware prompt."""
119
+
120
+ # Get all available tools first
121
+ all_schemas = self.tool_manager.get_tool_schemas(openai_format=True)
122
+ all_functions_dict = {schema['function']['name']: schema for schema in all_schemas}
123
+
124
+ # Use tool selection if enabled and not cached
125
+ if self.use_tool_selection and self.tool_selector and state.get("messages") and self._selected_tools_cache is None:
126
+ # Get the user's query from the first message
127
+ user_query = ""
128
+ for msg in state["messages"]:
129
+ if hasattr(msg, 'content') and msg.content:
130
+ user_query = msg.content
131
+ break
132
+
133
+ if user_query:
134
+ # Prepare tools for selection (convert schemas to tool info format)
135
+ available_tools = {}
136
+ for tool_name, schema in all_functions_dict.items():
137
+ available_tools[tool_name] = {
138
+ 'description': schema['function'].get('description', 'No description'),
139
+ 'source': 'tool_manager' # Could be enhanced to show actual source
140
+ }
141
+
142
+ # Select relevant tools using LLM (only once)
143
+ selected_tool_names = self.tool_selector.select_tools_for_task(
144
+ user_query, available_tools, max_tools=15
145
+ )
146
+
147
+ # Cache the selected tools
148
+ self._selected_tools_cache = {name: all_functions_dict[name]
149
+ for name in selected_tool_names
150
+ if name in all_functions_dict}
151
+
152
+ self.console.console.print(f"🎯 Selected {len(self._selected_tools_cache)} tools from {len(all_functions_dict)} available tools (cached for session)")
153
+ functions_dict = self._selected_tools_cache
154
+ else:
155
+ functions_dict = all_functions_dict
156
+ elif self.use_tool_selection and self._selected_tools_cache is not None:
157
+ # Use cached selected tools
158
+ functions_dict = self._selected_tools_cache
159
+ else:
160
+ # No tool selection or selection disabled
161
+ functions_dict = all_functions_dict
162
+
163
+ all_packages = self.package_manager.get_all_packages()
164
+ system_prompt = get_system_prompt(functions_dict, all_packages)
165
+
166
+ # Truncate conversation history to prevent context overflow
167
+ messages = [SystemMessage(content=system_prompt)] + state["messages"]
168
+
169
+ response = self.model.invoke(messages)
170
+
171
+ # Cut the text after the </execute> tag, while keeping the </execute> tag
172
+ if "</execute>" in response.content:
173
+ response.content = response.content.split("</execute>")[0] + "</execute>"
174
+
175
+ # Parse the response
176
+ msg = str(response.content)
177
+ llm_reply = AIMessage(content=msg.strip())
178
+
179
+ # Update step count
180
+ new_step_count = state.get("step_count", 0) + 1
181
+
182
+ return self.state_manager.create_state_dict(
183
+ messages=[llm_reply],
184
+ step_count=new_step_count,
185
+ error_count=state.get("error_count", 0),
186
+ start_time=state.get("start_time", time.time()),
187
+ current_plan=self._extract_current_plan(msg)
188
+ )
189
+
190
+ def _extract_current_plan(self, content: str) -> Optional[str]:
191
+ """Extract the current plan from the agent's response."""
192
+ return self.plan_manager.extract_plan_from_content(content)
193
+
194
+ def execute(self, state: AgentState) -> AgentState:
195
+ """Execute code using persistent Python executor."""
196
+ try:
197
+ last_message = state["messages"][-1].content
198
+ execute_match = re.search(r"<execute>(.*?)</execute>", last_message, re.DOTALL)
199
+
200
+ if execute_match:
201
+ code = execute_match.group(1).strip()
202
+
203
+ # Execute regular code in persistent environment (tools already injected)
204
+ result = self.python_executor(code)
205
+
206
+ # Include both the code and result in the observation
207
+ obs = f"\n<observation>\nCode Output:\n{result}</observation>"
208
+ return self.state_manager.create_state_dict(
209
+ messages=[AIMessage(content=obs.strip())],
210
+ step_count=state.get("step_count", 0),
211
+ error_count=state.get("error_count", 0),
212
+ start_time=state.get("start_time", time.time()),
213
+ current_plan=state.get("current_plan")
214
+ )
215
+ else:
216
+ return self.state_manager.create_state_dict(
217
+ messages=[AIMessage(content="<error>No executable code found</error>")],
218
+ step_count=state.get("step_count", 0),
219
+ error_count=state.get("error_count", 0) + 1,
220
+ start_time=state.get("start_time", time.time()),
221
+ current_plan=state.get("current_plan")
222
+ )
223
+ except Exception as e:
224
+ return self.state_manager.create_state_dict(
225
+ messages=[AIMessage(content=f"<error>Execution error: {str(e)}</error>")],
226
+ step_count=state.get("step_count", 0),
227
+ error_count=state.get("error_count", 0) + 1,
228
+ start_time=state.get("start_time", time.time()),
229
+ current_plan=state.get("current_plan")
230
+ )
231
+
232
+ def should_continue(self, state: AgentState) -> str:
233
+ """Decide whether to continue executing or end the workflow."""
234
+ last_message = state["messages"][-1].content
235
+ step_count = state.get("step_count", 0)
236
+ error_count = state.get("error_count", 0)
237
+ start_time = state.get("start_time", time.time())
238
+
239
+ # Check for timeout
240
+ if time.time() - start_time > self.config.timeout_seconds:
241
+ return "end"
242
+
243
+ # Check for maximum steps
244
+ if step_count >= self.config.max_steps:
245
+ return "end"
246
+
247
+ # Check for too many errors
248
+ if error_count >= self.config.retry_attempts:
249
+ return "end"
250
+
251
+ # Check if the finish() tool has been called
252
+ if "<solution>" in last_message and "</solution>" in last_message:
253
+ return "end"
254
+
255
+ # Check if there's an execute tag in the last message
256
+ elif "<execute>" in last_message and "</execute>" in last_message:
257
+ return "execute"
258
+
259
+ else:
260
+ return "end"
261
+
262
+ # ====================
263
+ # PACKAGE MANAGEMENT - Delegated to PackageManager
264
+ # ====================
265
+
266
+ def add_packages(self, packages: Dict[str, str]) -> bool:
267
+ """Add new packages to the available packages."""
268
+ return self.package_manager.add_packages(packages)
269
+
270
+ def get_all_packages(self) -> Dict[str, str]:
271
+ """Get all available packages (default + custom)."""
272
+ return self.package_manager.get_all_packages()
273
+
274
+ # ====================
275
+ # UNIFIED TOOL MANAGEMENT - Delegated to ToolManager
276
+ # ====================
277
+
278
+ def add_tool(self, function: callable, name: str = None, description: str = None) -> bool:
279
+ """Add a tool function to the manager."""
280
+ return self.tool_manager.add_tool(function, name, description, ToolSource.LOCAL)
281
+
282
+ def remove_tool(self, name: str) -> bool:
283
+ """Remove a tool by name."""
284
+ return self.tool_manager.remove_tool(name)
285
+
286
+ def list_tools(self, source: str = "all", include_details: bool = False) -> List[Dict]:
287
+ """List all available tools with optional filtering."""
288
+ source_enum = ToolSource.ALL
289
+ if source.lower() in ["local", "decorated", "mcp"]:
290
+ source_enum = ToolSource(source.lower())
291
+
292
+ return self.tool_manager.list_tools(source_enum, include_details)
293
+
294
+ def search_tools(self, query: str) -> List[Dict]:
295
+ """Search tools by name and description."""
296
+ return self.tool_manager.search_tools(query)
297
+
298
+ def get_tool_info(self, name: str) -> Optional[Dict]:
299
+ """Get detailed information about a specific tool."""
300
+ tool_info = self.tool_manager.get_tool(name)
301
+ if tool_info:
302
+ return {
303
+ "name": tool_info.name,
304
+ "description": tool_info.description,
305
+ "source": tool_info.source.value,
306
+ "server": tool_info.server,
307
+ "module": tool_info.module,
308
+ "has_function": tool_info.function is not None,
309
+ "required_parameters": tool_info.required_parameters,
310
+ "optional_parameters": tool_info.optional_parameters
311
+ }
312
+ return None
313
+
314
+ def get_all_tool_functions(self) -> Dict[str, callable]:
315
+ """Get all tool functions as a dictionary."""
316
+ return self.tool_manager.get_all_functions()
317
+
318
+ # ====================
319
+ # MCP METHODS - Now delegated to ToolManager
320
+ # ====================
321
+
322
+ def add_mcp(self, config_path: str = "./mcp_config.yaml") -> None:
323
+ """Add MCP tools from configuration file."""
324
+ self.tool_manager.add_mcp_server(config_path)
325
+
326
+ def list_mcp_tools(self) -> List[Dict]:
327
+ """List all loaded MCP tools."""
328
+ return self.tool_manager.list_tools(self.tool_manager.ToolSource.MCP)
329
+
330
+ def list_mcp_servers(self) -> Dict[str, List[str]]:
331
+ """List all MCP servers and their tools."""
332
+ return self.tool_manager.list_mcp_servers()
333
+
334
+ def show_mcp_status(self) -> None:
335
+ """Display detailed MCP status information to the user."""
336
+ self.tool_manager.show_mcp_status()
337
+
338
+ def get_mcp_summary(self) -> Dict[str, any]:
339
+ """Get a summary of MCP tools for programmatic access."""
340
+ return self.tool_manager.get_mcp_summary()
341
+
342
+ # ====================
343
+ # ENHANCED TOOL FEATURES
344
+ # ====================
345
+
346
+ def get_tool_statistics(self) -> Dict[str, any]:
347
+ """Get comprehensive tool statistics."""
348
+ return self.tool_manager.get_tool_statistics()
349
+
350
+ def validate_tools(self) -> Dict[str, List[str]]:
351
+ """Validate all tools and return any issues."""
352
+ return self.tool_manager.validate_tools()
353
+
354
+ # ====================
355
+ # TOOL SELECTION MANAGEMENT
356
+ # ====================
357
+
358
+ def reset_tool_selection(self):
359
+ """Reset the cached tool selection to allow re-selection on next query."""
360
+ self._selected_tools_cache = None
361
+ if self.use_tool_selection:
362
+ self.console.console.print("🔄 Tool selection cache cleared - will re-select tools on next query")
363
+
364
+ def get_selected_tools(self):
365
+ """Get the currently selected tools (if any)."""
366
+ return list(self._selected_tools_cache.keys()) if self._selected_tools_cache else None
367
+
368
+ # ====================
369
+ # TRACE AND SUMMARY METHODS
370
+ # ====================
371
+
372
+ def get_trace(self) -> Dict:
373
+ """Get the complete trace of the last execution."""
374
+ if not self.workflow_engine:
375
+ return {}
376
+
377
+ return {
378
+ "execution_time": time.strftime('%Y-%m-%d %H:%M:%S'),
379
+ "config": {
380
+ "max_steps": self.config.max_steps,
381
+ "timeout_seconds": self.config.timeout_seconds,
382
+ "verbose": self.config.verbose
383
+ },
384
+ "messages": self.workflow_engine.message_history,
385
+ "trace_logs": self.workflow_engine.trace_logs
386
+ }
387
+
388
+ def get_summary(self) -> Dict:
389
+ """Get a summary of the last execution."""
390
+ if not self.workflow_engine:
391
+ return {}
392
+ return self.workflow_engine.generate_summary()
393
+
394
+ def save_trace(self, filepath: str = None) -> str:
395
+ """Save the trace of the last execution to a file."""
396
+ if not self.workflow_engine:
397
+ raise RuntimeError("No workflow engine available")
398
+ return self.workflow_engine.save_trace_to_file(filepath)
399
+
400
+ def save_summary(self, filepath: str = None) -> str:
401
+ """Save the summary of the last execution to a file."""
402
+ if not self.workflow_engine:
403
+ raise RuntimeError("No workflow engine available")
404
+ return self.workflow_engine.save_summary_to_file(filepath)
405
+
406
+ # ====================
407
+ # PUBLIC INTERFACE
408
+ # ====================
409
+
410
+ def run(self, query: str, save_trace: bool = False, save_summary: bool = False,
411
+ trace_dir: str = "traces") -> str:
412
+ """
413
+ Run the agent with a given query using modular components.
414
+
415
+ Args:
416
+ query: The task/question to solve
417
+ save_trace: Whether to save the complete trace to a file
418
+ save_summary: Whether to save the execution summary to a file
419
+ trace_dir: Directory to save trace and summary files
420
+
421
+ Returns:
422
+ The final response content
423
+ """
424
+ # Start timing the overall execution
425
+ overall_timing = Timing(start_time=time.time())
426
+
427
+ # Display task header
428
+ self.console.print_task_header(query)
429
+
430
+ # Initialize agent with functions using ToolManager
431
+ functions_dict = self.get_all_tool_functions()
432
+
433
+ # Display enhanced tool information
434
+ # Get detailed tool statistics
435
+ stats = self.tool_manager.get_tool_statistics()
436
+ mcp_servers = self.tool_manager.list_mcp_servers()
437
+
438
+ self.console.console.print(f"🛠️ Loaded {stats['total_tools']} total tools:")
439
+ if stats['by_source']['local'] > 0:
440
+ self.console.console.print(f" 📋 Local tools: {stats['by_source']['local']}")
441
+ if stats['by_source']['decorated'] > 0:
442
+ self.console.console.print(f" 🎯 Decorated tools: {stats['by_source']['decorated']}")
443
+ if stats['by_source']['mcp'] > 0:
444
+ self.console.console.print(f" 🔗 MCP tools: {stats['by_source']['mcp']} from {len(mcp_servers)} servers")
445
+ for server_name, tools in mcp_servers.items():
446
+ self.console.console.print(f" • {server_name}: {len(tools)} tools")
447
+
448
+ # Inject functions into Python executor
449
+ self.python_executor.send_functions(functions_dict)
450
+
451
+ # Import available packages using PackageManager
452
+ imported_packages, failed_packages = self.package_manager.import_packages(self.python_executor)
453
+ self.console.print_packages_info(imported_packages, failed_packages)
454
+
455
+ # Inject any initial variables
456
+ state_variables = {}
457
+ self.python_executor.send_variables(state_variables)
458
+
459
+ # Create initial state using StateManager
460
+ input_state = self.state_manager.create_state_dict(
461
+ messages=[HumanMessage(content=query)],
462
+ step_count=0,
463
+ error_count=0,
464
+ start_time=time.time(),
465
+ current_plan=None
466
+ )
467
+
468
+ # Execute workflow using WorkflowEngine and get result with final state
469
+ result, final_state = self.workflow_engine.run_workflow(input_state)
470
+
471
+ # Complete overall timing and display summary
472
+ overall_timing.end_time = time.time()
473
+
474
+ # Extract final state information for summary
475
+ final_step_count = final_state.get("step_count", 0) if final_state else 0
476
+ final_error_count = final_state.get("error_count", 0) if final_state else 0
477
+
478
+ self.console.print_execution_summary(final_step_count, final_error_count, overall_timing.duration)
479
+
480
+ # Save trace and summary if requested
481
+ if save_trace or save_summary:
482
+ # Create trace directory if it doesn't exist
483
+ from pathlib import Path
484
+ trace_path = Path(trace_dir)
485
+ trace_path.mkdir(parents=True, exist_ok=True)
486
+
487
+ if save_trace:
488
+ trace_file = trace_path / f"agent_trace_{time.strftime('%Y%m%d_%H%M%S')}.json"
489
+ saved_trace = self.workflow_engine.save_trace_to_file(str(trace_file))
490
+ self.console.console.print(f"💾 Trace saved to: {saved_trace}")
491
+
492
+ if save_summary:
493
+ summary_file = trace_path / f"agent_summary_{time.strftime('%Y%m%d_%H%M%S')}.json"
494
+ saved_summary = self.workflow_engine.save_summary_to_file(str(summary_file))
495
+ self.console.console.print(f"📊 Summary saved to: {saved_summary}")
496
+
497
+ return result
498
+
499
+
500
+ # ====================
501
+ # EXAMPLE USAGE
502
+ # ====================
503
+
504
+ if __name__ == "__main__":
505
+ # Example usage of the fully modular CodeAgent architecture
506
+ # Create LLM client
507
+ model = ChatOpenAI(
508
+ model="google/gemini-2.5-flash",
509
+ base_url="https://openrouter.ai/api/v1",
510
+ temperature=0.7,
511
+ api_key=os.environ["OPENROUTER_API_KEY"],
512
+ )
513
+
514
+ model = ChatAnthropic(model='claude-sonnet-4-5-20250929')
515
+
516
+
517
+
518
+ # Create configuration
519
+ config = AgentConfig(
520
+ max_steps=15,
521
+ max_conversation_length=30,
522
+ retry_attempts=3,
523
+ timeout_seconds=1200,
524
+ verbose=True
525
+ )
526
+
527
+ # Create agent with unified tool management and LLM-based tool selection
528
+ agent = CodeAgent(model=model, config=config, use_tool_manager=True, use_tool_selection=True)
529
+
530
+ # Demonstrate tool management capabilities
531
+ print("\n🔧 Tool Management Demo:")
532
+
533
+ # Show tool statistics
534
+ stats = agent.get_tool_statistics()
535
+ print(f"📊 Tool Statistics: {stats}")
536
+
537
+ # Add MCP tools
538
+ try:
539
+ print("🔧 Loading MCP tools...")
540
+ script_dir = os.path.dirname(os.path.abspath(__file__))
541
+ config_path = os.path.join(script_dir, "mcp_config.yaml")
542
+ agent.add_mcp(config_path)
543
+ print("✅ MCP tools loaded successfully!")
544
+
545
+ # Show enhanced MCP status
546
+ agent.show_mcp_status()
547
+
548
+ # Show all available tools by source
549
+ all_tools = agent.list_tools("all", include_details=True)
550
+ print(f"\n📋 All tools loaded:")
551
+ for tool in all_tools:
552
+ print(f" • {tool['name']} ({tool['source']}) - {tool['description'][:50]}...")
553
+
554
+ except Exception as e:
555
+ print(f"⚠️ Could not load MCP tools: {e}")
556
+
557
+ # Validate tools
558
+ issues = agent.validate_tools()
559
+ if any(issues.values()):
560
+ print(f"⚠️ Tool validation issues: {issues}")
561
+ else:
562
+ print("✅ All tools validated successfully!")
563
+ # List available packages
564
+ print(f"\n📦 Available packages: {list(agent.get_all_packages().keys())}")
565
+ # Run the agent with a query and save trace/summary
566
+ print("\n🚀 Running agent with trace and summary saving...")
567
+ result = agent.run(
568
+ query="""
569
+ Use AlphaGenome MCP to analyze heart gene expression data to identify the causal gene
570
+ for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia. My API key is: AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U.
571
+ """,
572
+ save_trace=True, # Save complete execution trace
573
+ save_summary=True, # Save execution summary
574
+ trace_dir="traces" # Directory to save files
575
+ )
576
+
577
+ # You can also access trace and summary programmatically
578
+ print("\n📊 Execution Summary:")
579
+ summary = agent.get_summary()
580
+ print(f" Total steps: {summary.get('total_steps', 0)}")
581
+ print(f" Code executions: {len(summary.get('code_executions', []))}")
582
+ print(f" Observations: {len(summary.get('observations', []))}")
583
+ print(f" Errors: {len(summary.get('errors', []))}")
584
+
585
+ # You can save trace/summary manually after execution
586
+ # agent.save_trace("custom_trace.json")
587
+ # agent.save_summary("custom_summary.json")
app.py CHANGED
@@ -1,69 +1,656 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
2
 
3
- embed_html1 = """
4
- <iframe width="1120" height="620"
5
- src="https://www.youtube.com/embed/WmSC6zi8CIY?si=8h9HR1r9bZIpOaVN"
6
- title="YouTube video player" frameborder="0"
7
- allow="accelerometer; autoplay; clipboard-write; encrypted-media;
8
- gyroscope; picture-in-picture; web-share"
9
- referrerpolicy="strict-origin-when-cross-origin"
10
- allowfullscreen></iframe>
11
- """
12
 
13
- embed_html2 = """
14
- <iframe width="1120" height="620"
15
- src="https://www.youtube.com/embed/QMNqvybf_9U?si=Y1AAy5V_S7Nht4Vs"
16
- title="YouTube video player" frameborder="0"
17
- allow="accelerometer; autoplay; clipboard-write; encrypted-media;
18
- gyroscope; picture-in-picture; web-share"
19
- referrerpolicy="strict-origin-when-cross-origin"
20
- allowfullscreen></iframe>
21
- """
22
 
23
- with gr.Blocks() as demo:
24
- gr.Markdown(
25
- """
26
- # Demos
27
- Below, we showcase demos of AI agents created by Paper2Agent, illustrating how each agent applies the tools from its source paper to tackle scientific tasks.
28
 
29
- ## 🧬 AlphaGenome Agent for Genomic Data Interpretation
30
- **Example query:**
31
- ```
32
- Analyze heart gene expression data with AlphaGenome MCP to identify the causal genefor the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia.
33
- ```
34
- """
35
- )
36
- gr.HTML(embed_html1)
37
- gr.Markdown(
38
- """
39
- # 🤖 How to Create AlphaGenome and other Paper Agents?
40
-
41
- To streamline usage, we recommend creating Paper Agents by connecting Paper MCP servers to an AI coding agent, such as [Claude Code](https://www.anthropic.com/claude-code) or the [Google Gemini CLI](https://google-gemini.github.io/gemini-cli/) (it's free with a Google account!).
42
-
43
- We are also actively developing our own Chatbot-based agent, which will be released soon.
44
-
45
- ## ⚙️ Using Claude Code
46
- First, install and set up Claude Code:
47
- ```bash
48
- npm install -g @anthropic-ai/claude-code
49
- claude
50
- ```
51
- After setup, link Claude Code with the Paper MCP server of interest.
52
- For example, to create an AlphaGenome Agent, run:
53
- ```
54
- claude mcp add \
55
- --transport http \
56
- alphagenome \
57
- https://Paper2Agent-alphagenome-mcp.hf.space/mcp
58
 
59
- claude
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  ```
61
- ✅ You will now have an **AlphaGenome Agent** ready for genomics data interpretation.
62
-
63
- # 🔗 Connectable Paper MCP Servers
64
- * AlphaGenome: https://Paper2Agent-alphagenome-mcp.hf.space/mcp
65
- * Scanpy: https://Paper2Agent-scanpy-mcp.hf.space/mcp
66
- * TISSUE: https://Paper2Agent-tissue-mcp.hf.space/mcp
67
- """
68
- )
69
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio Interface for LangGraph ReAct Agent
3
+ A production-ready web interface with real-time streaming output
4
+ Styled similar to gradio_ui.py with sidebar layout
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import sys
10
+ import time
11
+ from typing import Generator, Optional
12
+ from dataclasses import dataclass
13
+
14
  import gradio as gr
15
+ from gradio.themes.utils import fonts
16
+ from dotenv import load_dotenv
17
+ from langchain_openai import ChatOpenAI
18
+ from langchain_core.messages import HumanMessage, AIMessage
19
+ from langchain_anthropic import ChatAnthropic
20
 
21
+ # Import the agent from agent_v3
22
+ sys.path.append(os.path.dirname(__file__))
23
+ from agent_v3 import CodeAgent
24
+ from core.types import AgentConfig
 
 
 
 
 
25
 
26
+ # Import support modules
27
+ from managers import PythonExecutor
 
 
 
 
 
 
 
28
 
29
+ # Load environment variables
30
+ load_dotenv("./.env")
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ class GradioAgentUI:
34
+ """
35
+ Gradio interface for interacting with the LangGraph ReAct Agent.
36
+ Styled similar to the smolagents GradioUI with sidebar layout.
37
+ """
38
+
39
+ def __init__(self, model=None, config=None):
40
+ """Initialize the Gradio Agent UI."""
41
+
42
+ # Default model
43
+ if model is None:
44
+ api_key = os.environ.get("OPENROUTER_API_KEY")
45
+ if not api_key:
46
+ raise ValueError(
47
+ "OPENROUTER_API_KEY environment variable is not set. "
48
+ "Please set it or provide a model instance."
49
+ )
50
+ # model = ChatOpenAI(
51
+ # model="google/gemini-2.5-flash",
52
+ # base_url="https://openrouter.ai/api/v1",
53
+ # temperature=0.7,
54
+ # api_key=api_key,
55
+ # )
56
+
57
+ model = ChatAnthropic(
58
+ model='claude-sonnet-4-5-20250929',
59
+ temperature=0.7,
60
+ api_key='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'
61
+ )
62
+
63
+
64
+
65
+
66
+ # Default config
67
+ if config is None:
68
+ config = AgentConfig(
69
+ max_steps=15,
70
+ max_conversation_length=30,
71
+ retry_attempts=3,
72
+ timeout_seconds=1200,
73
+ verbose=True
74
+ )
75
+
76
+ self.model = model
77
+ self.config = config
78
+ self.name = "AlphaGenome Agent Interface"
79
+ self.description = "AlphaGenome Agent is automatically created by [Paper2Agent](https://github.com/jmiao24/Paper2Agent) to use [AlphaGenome](https://github.com/google-deepmind/alphagenome) and interpret DNA variants."
80
+
81
+ def get_step_footnote(self, step_num: int, duration: float) -> str:
82
+ """Create a footnote for a step with timing information."""
83
+ return f'<span style="color: #888; font-size: 0.9em;">Step {step_num} | Duration: {duration:.2f}s</span>'
84
+
85
+ def format_code_block(self, code: str) -> str:
86
+ """Format code as a Python code block."""
87
+ code = code.strip()
88
+ if not code.startswith("```"):
89
+ code = f"```python\n{code}\n```"
90
+ return code
91
+
92
+ def extract_content_parts(self, message_content: str) -> dict:
93
+ """Extract different parts from the message content."""
94
+ parts = {
95
+ 'thinking': '',
96
+ 'plan': None,
97
+ 'code': None,
98
+ 'solution': None,
99
+ 'error': None,
100
+ 'observation': None
101
+ }
102
+
103
+ # Extract thinking (content before any tags)
104
+ thinking_pattern = r'^(.*?)(?=<(?:execute|solution|error|observation)|$)'
105
+ thinking_match = re.match(thinking_pattern, message_content, re.DOTALL)
106
+ if thinking_match:
107
+ thinking = thinking_match.group(1).strip()
108
+ # Remove plan from thinking
109
+ plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
110
+ thinking = re.sub(plan_pattern, '', thinking).strip()
111
+ # Remove any existing "Thinking:" or "Plan:" labels (including duplicates)
112
+ thinking = re.sub(r'(^|\n)(Thinking:|Plan:)\s*', '\n', thinking).strip()
113
+ # Remove any duplicate Thinking: that might remain
114
+ thinking = re.sub(r'Thinking:\s*', '', thinking).strip()
115
+ if thinking:
116
+ parts['thinking'] = thinking
117
+
118
+ # Extract plan
119
+ plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
120
+ plan_match = re.search(plan_pattern, message_content)
121
+ if plan_match:
122
+ parts['plan'] = plan_match.group(0)
123
+
124
+ # Extract code
125
+ code_match = re.search(r'<execute>(.*?)</execute>', message_content, re.DOTALL)
126
+ if code_match:
127
+ parts['code'] = code_match.group(1).strip()
128
+
129
+ # Extract solution
130
+ solution_match = re.search(r'<solution>(.*?)</solution>', message_content, re.DOTALL)
131
+ if solution_match:
132
+ parts['solution'] = solution_match.group(1).strip()
133
+
134
+ # Extract error
135
+ error_match = re.search(r'<error>(.*?)</error>', message_content, re.DOTALL)
136
+ if error_match:
137
+ parts['error'] = error_match.group(1).strip()
138
+
139
+ # Extract observation
140
+ obs_match = re.search(r'<observation>(.*?)</observation>', message_content, re.DOTALL)
141
+ if obs_match:
142
+ parts['observation'] = obs_match.group(1).strip()
143
+
144
+ return parts
145
+
146
+ def truncate_output(self, text: str, max_lines: int = 20) -> str:
147
+ """Truncate output to specified number of lines."""
148
+ lines = text.split('\n')
149
+ if len(lines) > max_lines:
150
+ truncated = '\n'.join(lines[:max_lines])
151
+ truncated += f"\n... (truncated {len(lines) - max_lines} lines)"
152
+ return truncated
153
+ return text
154
+
155
+ def stream_agent_response(self, agent: CodeAgent, query: str) -> Generator:
156
+ """Stream agent responses as Gradio ChatMessages with structured blocks."""
157
+
158
+ # Get all tool functions using the agent's method
159
+ tools_dict = agent.get_all_tool_functions()
160
+ agent.python_executor.send_functions(tools_dict)
161
+ agent.python_executor.send_variables({})
162
+
163
+ # Create initial state
164
+ input_state = {
165
+ "messages": [HumanMessage(content=query)],
166
+ "step_count": 0,
167
+ "error_count": 0,
168
+ "start_time": time.time(),
169
+ "current_plan": None
170
+ }
171
+
172
+ # Track state
173
+ displayed_reasoning = set()
174
+ previous_plan = None
175
+ step_timings = {}
176
+
177
+ try:
178
+ # Stream through the workflow execution
179
+ for state in agent.workflow_engine.graph.stream(input_state, stream_mode="values"):
180
+ step_count = state.get("step_count", 0)
181
+ error_count = state.get("error_count", 0)
182
+ current_plan = state.get("current_plan")
183
+
184
+ # Track timing
185
+ if step_count not in step_timings:
186
+ step_timings[step_count] = time.time()
187
+
188
+ message = state["messages"][-1]
189
+
190
+ if isinstance(message, AIMessage):
191
+ content = message.content
192
+ parts = self.extract_content_parts(content)
193
+
194
+ # First yield step header only
195
+ if step_count > 0:
196
+ step_header = f"## Step {step_count}\n"
197
+ yield gr.ChatMessage(
198
+ role="assistant",
199
+ content=step_header,
200
+ metadata={"status": "done"}
201
+ )
202
+
203
+ # Display Thinking block (styled like Current Plan)
204
+ if parts['thinking'] and len(parts['thinking']) > 20:
205
+ thinking_text = parts['thinking']
206
+ # Clean up any remaining labels
207
+ thinking_text = re.sub(r'(Thinking:|Plan:)\s*', '', thinking_text).strip()
208
+ # Clean up excessive whitespace - replace multiple newlines with max 2
209
+ thinking_text = re.sub(r'\n\s*\n\s*\n+', '\n\n', thinking_text).strip()
210
+ # Also clean up any leading/trailing whitespace on each line
211
+ thinking_text = '\n'.join(line.strip() for line in thinking_text.split('\n') if line.strip())
212
+
213
+ content_hash = hash(thinking_text)
214
+ if content_hash not in displayed_reasoning and thinking_text:
215
+ thinking_block = f"""<div style="background-color: #f8f9fa; border-left: 4px solid #6b7280; padding: 12px; margin: 10px 0; border-radius: 4px;">
216
+ <div style="font-weight: bold; color: #374151; margin-bottom: 8px;">🤔 Thinking</div>
217
+ <div style="margin: 0; white-space: pre-line; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #000000; background-color: #ffffff; padding: 10px; border-radius: 4px; font-size: 14px; line-height: 1.6;">{thinking_text}</div>
218
+ </div>"""
219
+ yield gr.ChatMessage(
220
+ role="assistant",
221
+ content=thinking_block,
222
+ metadata={"status": "done"}
223
+ )
224
+ displayed_reasoning.add(content_hash)
225
+
226
+ # Then display Current Plan after thinking
227
+ if current_plan and current_plan != previous_plan:
228
+ formatted_plan = current_plan.replace('[ ]', '☐').replace('[✓]', '✅').replace('[✗]', '❌')
229
+
230
+ plan_block = f"""<div style="background-color: #f8f9fa; border-left: 4px solid #000000; padding: 12px; margin: 10px 0; border-radius: 4px;">
231
+ <div style="font-weight: bold; color: #000000; margin-bottom: 8px;">📋 Current Plan</div>
232
+ <pre style="margin: 0; white-space: pre-wrap; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; color: #000000; background-color: #ffffff; padding: 10px; border-radius: 4px; font-size: 14px; line-height: 1.6;">{formatted_plan}</pre>
233
+ </div>"""
234
+ yield gr.ChatMessage(
235
+ role="assistant",
236
+ content=plan_block,
237
+ metadata={"status": "done"}
238
+ )
239
+ previous_plan = current_plan
240
+
241
+ # Display Executing Code as independent block with proper syntax highlighting
242
+ if parts['code']:
243
+ # Format as markdown code block for proper rendering
244
+ code_block = f"""<div style="background-color: #f7fafc; border-left: 4px solid #48bb78; padding: 12px; margin: 10px 0; border-radius: 4px;">
245
+ <div style="font-weight: bold; color: #22543d; margin-bottom: 8px;">⚡ Executing Code</div>
246
+ </div>
247
+
248
+ ```python
249
+ {parts['code']}
250
+ ```"""
251
+ yield gr.ChatMessage(
252
+ role="assistant",
253
+ content=code_block,
254
+ metadata={"status": "done"}
255
+ )
256
+
257
+ # Display Execution Result as independent block
258
+ if parts['observation']:
259
+ truncated = self.truncate_output(parts['observation'])
260
+
261
+ result_block = f"""<div style="background-color: #fef5e7; border-left: 4px solid #f6ad55; padding: 12px; margin: 10px 0; border-radius: 4px;">
262
+ <div style="font-weight: bold; color: #744210; margin-bottom: 8px;">📊 Execution Result</div>
263
+ </div>
264
+
265
  ```
266
+ {truncated}
267
+ ```"""
268
+ yield gr.ChatMessage(
269
+ role="assistant",
270
+ content=result_block,
271
+ metadata={"status": "done"}
272
+ )
273
+
274
+ # Display solution with special formatting as independent block
275
+ if parts['solution']:
276
+ solution_block = f"""<div style="background-color: #d1fae5; border-left: 4px solid #10b981; padding: 16px; margin: 10px 0; border-radius: 6px;">
277
+ <div style="font-weight: bold; color: #065f46; margin-bottom: 12px; font-size: 16px;">✅ Final Solution</div>
278
+ <div class="solution-content" style="color: #1f2937 !important; background-color: #ffffff; padding: 16px; border-radius: 4px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 15px; line-height: 1.7; border: 1px solid #e5e7eb;">
279
+
280
+ {parts['solution']}
281
+
282
+ </div>
283
+ <style>
284
+ .solution-content, .solution-content * {{
285
+ color: #1f2937 !important;
286
+ background-color: transparent !important;
287
+ }}
288
+ .solution-content code, .solution-content pre {{
289
+ background-color: #f3f4f6 !important;
290
+ color: #1f2937 !important;
291
+ padding: 2px 6px;
292
+ border-radius: 3px;
293
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
294
+ }}
295
+ .solution-content pre {{
296
+ padding: 12px;
297
+ overflow-x: auto;
298
+ }}
299
+ .solution-content table {{
300
+ border-collapse: collapse;
301
+ width: 100%;
302
+ margin: 10px 0;
303
+ background-color: #ffffff !important;
304
+ }}
305
+ .solution-content td, .solution-content th {{
306
+ border: 1px solid #d1d5db;
307
+ padding: 8px;
308
+ color: #1f2937 !important;
309
+ background-color: #ffffff !important;
310
+ }}
311
+ .solution-content th {{
312
+ background-color: #f3f4f6 !important;
313
+ font-weight: 600;
314
+ }}
315
+ .solution-content p, .solution-content div, .solution-content span {{
316
+ background-color: transparent !important;
317
+ }}
318
+ </style>
319
+ </div>"""
320
+ yield gr.ChatMessage(
321
+ role="assistant",
322
+ content=solution_block,
323
+ metadata={"status": "done"}
324
+ )
325
+
326
+ # Display error with special formatting
327
+ if parts['error']:
328
+ error_block = f"""<div style="background-color: #fed7d7; border-left: 4px solid #fc8181; padding: 12px; margin: 10px 0; border-radius: 4px;">
329
+ <div style="font-weight: bold; color: #742a2a; margin-bottom: 8px;">⚠️ Error</div>
330
+ <div style="color: #742a2a;">{parts['error']}</div>
331
+ </div>"""
332
+ yield gr.ChatMessage(
333
+ role="assistant",
334
+ content=error_block,
335
+ metadata={"status": "done"}
336
+ )
337
+
338
+ # Add step footnote with timing ONLY after Execution Result
339
+ if parts['observation'] and step_count in step_timings:
340
+ duration = time.time() - step_timings[step_count]
341
+ footnote = f'<div style="color: #718096; font-size: 0.875em; margin-top: 8px;">Step {step_count} | Duration: {duration:.2f}s</div>'
342
+ yield gr.ChatMessage(
343
+ role="assistant",
344
+ content=footnote,
345
+ metadata={"status": "done"}
346
+ )
347
+
348
+ # Add separator between steps
349
+ if step_count > 0:
350
+ yield gr.ChatMessage(
351
+ role="assistant",
352
+ content='<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 20px 0;">',
353
+ metadata={"status": "done"}
354
+ )
355
+
356
+ except Exception as e:
357
+ error_block = f"""<div style="background-color: #fed7d7; border-left: 4px solid #fc8181; padding: 12px; margin: 10px 0; border-radius: 4px;">
358
+ <div style="font-weight: bold; color: #742a2a; margin-bottom: 8px;">💥 Critical Error</div>
359
+ <div style="color: #742a2a;">Error during agent execution: {str(e)}</div>
360
+ </div>"""
361
+ yield gr.ChatMessage(
362
+ role="assistant",
363
+ content=error_block,
364
+ metadata={"status": "done"}
365
+ )
366
+
367
+ def interact_with_agent(self, prompt: str, api_key: str, messages: list, session_state: dict) -> Generator:
368
+ """Handle interaction with the agent."""
369
+
370
+ # Store original user input for display
371
+ original_prompt = prompt
372
+
373
+ # Add API key to the prompt if provided (for agent processing)
374
+ if api_key and api_key.strip():
375
+ prompt = f"{prompt} My API key is: {api_key.strip()}."
376
+
377
+ # Initialize agent in session if needed
378
+ if "agent" not in session_state:
379
+ session_state["agent"] = CodeAgent(
380
+ model=self.model,
381
+ config=self.config,
382
+ use_tool_manager=True,
383
+ use_tool_selection=False # Disable for Gradio UI by default
384
+ )
385
+
386
+ # Try to load MCP config if available
387
+ mcp_config_path = "./mcp_config.yaml"
388
+ if os.path.exists(mcp_config_path):
389
+ try:
390
+ session_state["agent"].add_mcp(mcp_config_path)
391
+ print(f"✅ Loaded MCP tools from {mcp_config_path}")
392
+ except Exception as e:
393
+ print(f"⚠️ Could not load MCP tools: {e}")
394
+
395
+ agent = session_state["agent"]
396
+
397
+ try:
398
+ # Add user message immediately (use original prompt for display)
399
+ messages.append(gr.ChatMessage(role="user", content=original_prompt, metadata={"status": "done"}))
400
+ yield messages
401
+
402
+ # Add a "thinking" indicator immediately
403
+ messages.append(gr.ChatMessage(role="assistant", content="🤔 Processing...", metadata={"status": "pending"}))
404
+ yield messages
405
+
406
+ # Remove the thinking indicator before adding real content
407
+ messages.pop()
408
+
409
+ # Stream agent responses with real-time updates
410
+ for msg in self.stream_agent_response(agent, prompt):
411
+ messages.append(msg)
412
+ yield messages
413
+
414
+ except Exception as e:
415
+ messages.append(
416
+ gr.ChatMessage(
417
+ role="assistant",
418
+ content=f"Error: {str(e)}",
419
+ metadata={"title": "💥 Error", "status": "done"}
420
+ )
421
+ )
422
+ yield messages
423
+
424
+ def create_app(self):
425
+ """Create the Gradio app with sidebar layout."""
426
+
427
+ # Create custom theme with modern fonts
428
+ modern_theme = gr.themes.Monochrome(
429
+ font=fonts.GoogleFont("Inter"),
430
+ font_mono=fonts.GoogleFont("JetBrains Mono")
431
+ )
432
+
433
+ with gr.Blocks(theme=modern_theme, fill_height=True, title=self.name, css="""
434
+ /* Hide Gradio footer */
435
+ .footer {display: none !important;}
436
+ footer {display: none !important;}
437
+ .gradio-footer {display: none !important;}
438
+ #footer {display: none !important;}
439
+ [class*="footer"] {display: none !important;}
440
+ [id*="footer"] {display: none !important;}
441
+ .block.svelte-1scc9gv {display: none !important;}
442
+ .built-with-gradio {display: none !important;}
443
+ .gradio-container footer {display: none !important;}
444
+ """) as demo:
445
+ # Force light theme and hide footer
446
+ demo.load(js="""
447
+ () => {
448
+ document.body.classList.remove('dark');
449
+ document.querySelector('gradio-app').classList.remove('dark');
450
+ const url = new URL(window.location);
451
+ url.searchParams.set('__theme', 'light');
452
+ window.history.replaceState({}, '', url);
453
+
454
+ // Hide footer elements
455
+ setTimeout(() => {
456
+ const footers = document.querySelectorAll('footer, .footer, .gradio-footer, #footer, [class*="footer"], [id*="footer"], .built-with-gradio');
457
+ footers.forEach(footer => footer.style.display = 'none');
458
+
459
+ // Also hide any Gradio logo/branding
460
+ const brandingElements = document.querySelectorAll('a[href*="gradio"], .gradio-logo, [alt*="gradio"]');
461
+ brandingElements.forEach(el => el.style.display = 'none');
462
+ }, 100);
463
+ }
464
+ """)
465
+ # Session state
466
+ session_state = gr.State({})
467
+ stored_messages = gr.State([])
468
+
469
+ with gr.Row():
470
+ # Sidebar
471
+ with gr.Column(scale=1):
472
+ gr.Markdown(f"# {self.name}")
473
+ gr.Markdown(f"> {self.description}")
474
+
475
+ gr.Markdown("---")
476
+
477
+ # API Key section
478
+ with gr.Group():
479
+ gr.Markdown("**AlphaGenome API Key**")
480
+ api_key_input = gr.Textbox(
481
+ label="API Key",
482
+ type="password",
483
+ placeholder="Enter your AlphaGenome API key",
484
+ value="AIzaSyD1USDNy9WqfIROICB3FWI1wJHmkO2z21U"
485
+ )
486
+
487
+ # Input section
488
+ with gr.Group():
489
+ gr.Markdown("**Your Request**")
490
+ text_input = gr.Textbox(
491
+ lines=4,
492
+ label="Query",
493
+ placeholder="Enter your query here and press Enter or click Submit",
494
+ value="""Use AlphaGenome MCP to analyze heart gene expression data to identify the causal gene
495
+ for the variant chr11:116837649:T>G, associated with Hypoalphalipoproteinemia."""
496
+ )
497
+ submit_btn = gr.Button("🚀 Submit", variant="primary", size="lg")
498
+
499
+ # Configuration section
500
+ with gr.Accordion("⚙️ Configuration", open=False):
501
+ max_steps_input = gr.Slider(
502
+ label="Max Steps",
503
+ minimum=5,
504
+ maximum=50,
505
+ value=self.config.max_steps,
506
+ step=1
507
+ )
508
+
509
+ temperature_input = gr.Slider(
510
+ label="Temperature",
511
+ minimum=0.0,
512
+ maximum=1.0,
513
+ value=0.7,
514
+ step=0.1
515
+ )
516
+
517
+ apply_config_btn = gr.Button("Apply Configuration", size="sm")
518
+
519
+ # Example queries
520
+ with gr.Accordion("📚 Example Queries", open=False):
521
+ gr.Examples(
522
+ examples=[
523
+ ["What's the quantile score of chr3:197081044:TACTC>T on splice junction in Artery (tibial)?"],
524
+ ["What are the raw and quantile scores of the variant chr3:120280774:G>T for chromatin accessibility in the GM12878 cell line?"],
525
+ ],
526
+ inputs=text_input,
527
+ )
528
+
529
+ gr.Markdown("---")
530
+ # gr.HTML(
531
+ # "<center><small>Powered by LangGraph & OpenRouter</small></center>"
532
+ # )
533
+
534
+ # Main chat area
535
+ with gr.Column(scale=3):
536
+ chatbot = gr.Chatbot(
537
+ label="Agent Conversation",
538
+ type="messages",
539
+ height=700,
540
+ show_copy_button=True,
541
+ avatar_images=(
542
+ None, # Default user avatar
543
+ "images.png" # Assistant avatar
544
+ ),
545
+ latex_delimiters=[
546
+ {"left": r"$$", "right": r"$$", "display": True},
547
+ {"left": r"$", "right": r"$", "display": False},
548
+ ],
549
+ render_markdown=True,
550
+ sanitize_html=False, # Allow custom HTML for better formatting
551
+ )
552
+
553
+ with gr.Row():
554
+ clear_btn = gr.Button("🗑️ Clear", size="sm")
555
+ # Note: stop_btn is created but not wired up yet
556
+ # This could be used for future cancellation functionality
557
+ stop_btn = gr.Button("⏹️ Stop", size="sm", variant="stop")
558
+
559
+ # Event handlers
560
+ def update_config(max_steps, temperature, session_state):
561
+ """Update agent configuration."""
562
+ if "agent" in session_state:
563
+ agent = session_state["agent"]
564
+ agent.config.max_steps = max_steps
565
+ if hasattr(agent.model, 'temperature'):
566
+ agent.model.temperature = temperature
567
+ return "Configuration updated!"
568
+
569
+ def clear_chat():
570
+ """Clear the chat."""
571
+ return [], []
572
+
573
+ # Wire up events
574
+ text_input.submit(
575
+ lambda x: ("", gr.Button(interactive=False)),
576
+ [text_input],
577
+ [text_input, submit_btn]
578
+ ).then(
579
+ self.interact_with_agent,
580
+ [stored_messages, api_key_input, chatbot, session_state],
581
+ [chatbot]
582
+ ).then(
583
+ lambda: gr.Button(interactive=True),
584
+ None,
585
+ [submit_btn]
586
+ )
587
+
588
+ submit_btn.click(
589
+ lambda x: (x, "", gr.Button(interactive=False)),
590
+ [text_input],
591
+ [stored_messages, text_input, submit_btn]
592
+ ).then(
593
+ self.interact_with_agent,
594
+ [stored_messages, api_key_input, chatbot, session_state],
595
+ [chatbot]
596
+ ).then(
597
+ lambda: gr.Button(interactive=True),
598
+ None,
599
+ [submit_btn]
600
+ )
601
+
602
+ apply_config_btn.click(
603
+ update_config,
604
+ [max_steps_input, temperature_input, session_state],
605
+ None
606
+ )
607
+
608
+ clear_btn.click(
609
+ clear_chat,
610
+ None,
611
+ [chatbot, stored_messages]
612
+ )
613
+
614
+ return demo
615
+
616
+ def launch(self, share: bool = False, **kwargs):
617
+ """Launch the Gradio app."""
618
+ app = self.create_app()
619
+ # Set defaults if not provided in kwargs
620
+ kwargs.setdefault('server_name', '0.0.0.0')
621
+ kwargs.setdefault('server_port', 7860)
622
+ kwargs.setdefault('show_error', True)
623
+
624
+ app.queue(max_size=10).launch(
625
+ share=share,
626
+ show_api=False, # 隐藏 "Use via API" 链接
627
+ favicon_path=None, # 移除默认favicon
628
+ **kwargs
629
+ )
630
+
631
+
632
+ if __name__ == "__main__":
633
+ # Check for API key before starting
634
+ if not os.environ.get("OPENROUTER_API_KEY"):
635
+ print("\n⚠️ Error: OPENROUTER_API_KEY environment variable is not set!")
636
+ print("\nPlease set it using one of these methods:")
637
+ print("1. Export it in your shell:")
638
+ print(" export OPENROUTER_API_KEY='your-api-key-here'")
639
+ print("\n2. Create a .env file in the current directory with:")
640
+ print(" OPENROUTER_API_KEY=your-api-key-here")
641
+ print("\n3. Or provide your own model instance when initializing GradioAgentUI")
642
+ sys.exit(1)
643
+
644
+ try:
645
+ # Create and launch the interface with optional MCP config
646
+ ui = GradioAgentUI()
647
+
648
+ # Optional: Load MCP tools if config exists
649
+ mcp_config_path = "./mcp_config.yaml"
650
+ if os.path.exists(mcp_config_path):
651
+ print(f"Found MCP config at {mcp_config_path}")
652
+
653
+ ui.launch(share=False, quiet=False)
654
+ except Exception as e:
655
+ print(f"\n❌ Error starting Gradio interface: {e}")
656
+ sys.exit(1)
core/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core module for CodeAct agent - contains shared types and utilities.
3
+ """
4
+
5
+ from .types import AgentState, AgentConfig
6
+ from .constants import LIBRARY_CONTENT_DICT, SYSTEM_PROMPT_TEMPLATE
7
+
8
+ __all__ = ['AgentState', 'AgentConfig', 'LIBRARY_CONTENT_DICT', 'SYSTEM_PROMPT_TEMPLATE']
core/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (508 Bytes). View file
 
core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (449 Bytes). View file
 
core/__pycache__/constants.cpython-311.pyc ADDED
Binary file (7.6 kB). View file
 
core/__pycache__/constants.cpython-312.pyc ADDED
Binary file (7.58 kB). View file
 
core/__pycache__/types.cpython-311.pyc ADDED
Binary file (1.78 kB). View file
 
core/__pycache__/types.cpython-312.pyc ADDED
Binary file (1.49 kB). View file
 
core/constants.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Constants and templates for CodeAct agent.
3
+ """
4
+
5
+ # Available packages with descriptions
6
+ LIBRARY_CONTENT_DICT = {
7
+ "numpy": "[Python Package] The fundamental package for scientific computing with Python, providing support for arrays, matrices, and mathematical functions.",
8
+ "scipy": "[Python Package] A Python library for scientific and technical computing, including modules for optimization, linear algebra, integration, and statistics.",
9
+ }
10
+
11
+ # System prompt template
12
+ SYSTEM_PROMPT_TEMPLATE = """You are an expert assistant who can solve any task using Python code execution. You will be given a task to solve as best you can.
13
+
14
+ ## Planning & Execution Workflow
15
+
16
+ ### Step 1: Make a Plan
17
+ For every task, start by creating a clear, numbered plan with checkboxes:
18
+ ```
19
+ 1. [ ] First step
20
+ 2. [ ] Second step
21
+ 3. [ ] Third step
22
+ ```
23
+
24
+ ### Step 2: Execute & Update
25
+ After completing each step, update the checklist with [✓]:
26
+ ```
27
+ 1. [✓] First step (completed)
28
+ 2. [ ] Second step
29
+ 3. [ ] Third step
30
+ ```
31
+
32
+ If a step fails, mark it with [✗] and add a corrective step:
33
+ ```
34
+ 1. [✓] First step (completed)
35
+ 2. [✗] Second step (failed because...)
36
+ 3. [ ] Corrected second step
37
+ 4. [ ] Third step
38
+ ```
39
+
40
+ **Always show the updated plan after each step** so progress can be tracked.
41
+
42
+ ## Response Format
43
+
44
+ At each turn, first provide your **thinking and reasoning** based on the conversation history.
45
+
46
+ Then choose ONE of these two options:
47
+
48
+ ### Option 1: Execute Code (<execute> tag)
49
+ Use this to run Python code and get results. The output will appear in <observation></observation> tags.
50
+
51
+ **Format:**
52
+ ```
53
+ <execute>
54
+ print("Hello World!")
55
+ </execute>
56
+ ```
57
+
58
+ **CRITICAL**: Always end with `</execute>` tag.
59
+
60
+ ### Option 2: Provide Solution (<solution> tag)
61
+ Use this when you have the final answer ready to present to the user.
62
+
63
+ **Format:**
64
+ ```
65
+ The answer is <solution> Your final answer </solution>
66
+ ```
67
+
68
+ **CRITICAL**:
69
+ - Always end with `</solution>` tag.
70
+ - **Always provide a comprehensive summary** in your final solution that includes:
71
+ - A clear statement of what was accomplished
72
+ - Key results, findings, or outputs
73
+ - Any important data, numbers, or visualizations generated
74
+ - Steps taken to reach the solution (brief overview)
75
+ - Any files created or modified
76
+ - Next steps or recommendations (if applicable)
77
+
78
+ ## Code Execution Best Practices
79
+
80
+ ### Printing Results
81
+ **YOU MUST print outputs** - the system cannot see unpublished results:
82
+ ```python
83
+ # ✓ CORRECT
84
+ result = some_function(param1, param2)
85
+ print(result)
86
+
87
+ # ✗ WRONG - output is invisible
88
+ some_function(param1, param2)
89
+ ```
90
+
91
+ ### Working with Data
92
+ When working with data structures, always inspect them **but limit output to avoid context overflow**:
93
+ ```python
94
+ # For DataFrames - ALWAYS use head() for large tables
95
+ print(f"Shape: {df.shape}")
96
+ print(f"Columns: {list(df.columns)}")
97
+ print(df.head(10)) # Show first 10 rows max
98
+
99
+ # For large DataFrames, also show basic info
100
+ if len(df) > 20:
101
+ print(f"DataFrame has {len(df)} rows. Showing first 10 rows only.")
102
+
103
+ # For lists/dicts
104
+ print(f"Length: {len(data)}")
105
+ print(data[:5]) # Show first few items
106
+
107
+ # For any large output, use head() or limit display
108
+ if hasattr(data, 'head'):
109
+ print(data.head(10)) # For pandas objects
110
+ else:
111
+ print(str(data)[:1000] + "..." if len(str(data)) > 1000 else data)
112
+ ```
113
+
114
+ ### Function Calls
115
+ When calling available functions, **capture and print the output**:
116
+ ```python
117
+ result = function_name(param1="value1", param2="value2")
118
+ print(f"Function result: {result}")
119
+ ```
120
+
121
+ ### Creating Input Files for Functions
122
+ If a function requires a file as input (e.g., CSV, Excel, TSV), **you must create the file first** with the exact columns specified in the function's parameter description:
123
+
124
+ ```python
125
+ # Example: If function requires a CSV with specific columns
126
+ import pandas as pd
127
+
128
+ # Create DataFrame with required columns (as specified in function parameters)
129
+ df = pd.DataFrame({
130
+ 'column1': [...], # Fill with appropriate data
131
+ 'column2': [...], # Fill with appropriate data
132
+ 'column3': [...] # Fill with appropriate data
133
+ })
134
+
135
+ # Save to file
136
+ df.to_csv('input_file.csv', index=False)
137
+ print(f"Created input file with shape: {df.shape}")
138
+
139
+ # Then call the function
140
+ result = function_name(file_path='input_file.csv')
141
+ print(f"Function result: {result}")
142
+ ```
143
+
144
+ **Important**: Always check the function's parameter descriptions to understand:
145
+ - What columns are required in the input file
146
+ - The expected format of each column
147
+ - Any specific data types or constraints
148
+
149
+ ### Code Quality
150
+ - Keep code simple and readable
151
+ - Add comments for clarity
152
+ - Print intermediate results to create a clear execution log
153
+ - Handle errors gracefully
154
+
155
+ ## Important Rules
156
+
157
+ 1. **Every response must include EXACTLY ONE tag**: Either `<execute>` or `<solution>`
158
+ 2. **Never use both tags in the same response**
159
+ 3. **Never respond without any tags**
160
+ 4. **Always close tags properly** with `</execute>` or `</solution>`
161
+ 5. **Always print outputs** when executing code - unpublished results are invisible to the system
162
+
163
+ ## Examples
164
+
165
+ ### Example 1: Executing Code
166
+
167
+ **User Query**: "Calculate the factorial of 10"
168
+
169
+ **Correct Response**:
170
+ ```
171
+ I'll calculate the factorial of 10 using Python.
172
+
173
+ <execute>
174
+ import math
175
+ result = math.factorial(10)
176
+ print(f"The factorial of 10 is: {result}")
177
+ </execute>
178
+ ```
179
+
180
+ **WRONG Response** (missing tags):
181
+ ```
182
+ I'll calculate the factorial of 10.
183
+ import math
184
+ result = math.factorial(10)
185
+ print(result)
186
+ ```
187
+
188
+ ### Example 2: Using Available Functions
189
+
190
+ **User Query**: "Add 5 and 3"
191
+
192
+ **Correct Response**:
193
+ ```
194
+ I'll use the add_numbers function to add 5 and 3.
195
+
196
+ <execute>
197
+ result = add_numbers(5, 3)
198
+ print(f"Result: {result}")
199
+ </execute>
200
+ ```
201
+
202
+ ### Example 3: Providing Final Solution
203
+
204
+ **User Query**: "What is 2+2?"
205
+
206
+ **Correct Response**:
207
+ ```
208
+ <execute>
209
+ result = 2 + 2
210
+ print(f"2 + 2 = {result}")
211
+ </execute>
212
+ ```
213
+
214
+ Then after seeing the output:
215
+ ```
216
+ <solution>
217
+ The answer is 4. I calculated 2 + 2 and got the result of 4.
218
+ </solution>
219
+ ```
220
+
221
+ **CRITICAL REMINDER**:
222
+ - ALWAYS wrap code in `<execute>` tags
223
+ - ALWAYS wrap final answers in `<solution>` tags
224
+ - NEVER output code without `<execute>` tags
225
+
226
+ ## Available Functions
227
+
228
+ You have access to the following functions. These functions are already available in your Python environment and can be called directly:
229
+
230
+ {% for func_name, schema in functions.items() %}
231
+ **{{ schema.function.name }}({% for param_name in schema.function.parameters.properties.keys() %}{{ param_name }}{{ ", " if not loop.last }}{% endfor %})**
232
+ - Description: {{ schema.function.description }}
233
+ - Parameters:
234
+ {% for param_name, param_info in schema.function.parameters.properties.items() %}
235
+ - {{ param_name }} ({{ param_info.type }}{% if param_info.enum %}, choices: {{ param_info.enum }}{% endif %}): {{ param_info.description }}{% if 'default' in param_info %} [default: {{ param_info.default }}]{% endif %}{% if param_name in schema.function.parameters.required %} [REQUIRED]{% endif %}
236
+ {% endfor %}
237
+ - Example: result = {{ schema.function.name }}({% for param_name in schema.function.parameters.properties.keys() %}{{ param_name }}=example_value{{ ", " if not loop.last }}{% endfor %})
238
+ {% if schema.function.parameters.required %}
239
+ - **Important**: This function requires the following parameters: {{ schema.function.parameters.required|join(', ') }}
240
+ {% endif %}
241
+
242
+ {% endfor %}
243
+
244
+ Important: These functions are pre-loaded in your Python environment. Call them directly like regular Python functions.
245
+ **CRITICAL**: Always provide ALL required parameters when calling functions. Check the [REQUIRED] markers above.
246
+
247
+ ## Available Python Packages
248
+
249
+ The following Python packages are available in your environment and can be imported directly:
250
+
251
+ {% for package_name, description in packages.items() %}
252
+ **{{ package_name }}**
253
+ - {{ description }}
254
+ - Import: `import {{ package_name }}`
255
+
256
+ {% endfor %}
257
+
258
+ Remember to import these packages before using them in your code.
259
+ """
core/types.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shared type definitions for CodeAct agent.
3
+ """
4
+
5
+ from typing import Annotated, List, Optional, TypedDict
6
+ from dataclasses import dataclass
7
+ from langchain_core.messages import BaseMessage
8
+ from langgraph.graph.message import add_messages
9
+
10
+
11
+ @dataclass
12
+ class AgentConfig:
13
+ """Configuration for the Code Agent."""
14
+ max_steps: int = 20
15
+ max_conversation_length: int = 50
16
+ retry_attempts: int = 3
17
+ timeout_seconds: int = 900
18
+ verbose: bool = True
19
+ memory_window: int = 15 # Keep last N messages for context
20
+
21
+
22
+ class AgentState(TypedDict):
23
+ """State type for the agent workflow."""
24
+ # reducer tells LangGraph to append new messages
25
+ messages: Annotated[List[BaseMessage], add_messages]
26
+ step_count: int
27
+ error_count: int
28
+ start_time: float
29
+ current_plan: Optional[str]
images.png ADDED
managers/.DS_Store ADDED
Binary file (8.2 kB). View file
 
managers/__init__.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Manager modules for CodeAct agent - organized by subsystem.
3
+
4
+ Subsystems:
5
+ - execution: Python execution and monitoring
6
+ - tools: Tool management, registry, and integrations
7
+ - workflow: Workflow execution, state, and plan management
8
+ - support: Console display and package management
9
+ """
10
+
11
+ # Execution subsystem
12
+ from .execution import PythonExecutor, Timing, TokenUsage
13
+
14
+ # Tools subsystem
15
+ from .tools import ToolManager, ToolSource, ToolRegistry, ToolSelector, MCPManager
16
+
17
+ # Workflow subsystem
18
+ from .workflow import WorkflowEngine, StateManager, PlanManager
19
+
20
+ # Support subsystem
21
+ from .support import ConsoleDisplay, PackageManager
22
+
23
+ __all__ = [
24
+ # Execution
25
+ 'PythonExecutor',
26
+ 'Timing',
27
+ 'TokenUsage',
28
+ # Tools
29
+ 'ToolManager',
30
+ 'ToolSource',
31
+ 'ToolRegistry',
32
+ 'ToolSelector',
33
+ 'MCPManager',
34
+ # Workflow
35
+ 'WorkflowEngine',
36
+ 'StateManager',
37
+ 'PlanManager',
38
+ # Support
39
+ 'ConsoleDisplay',
40
+ 'PackageManager'
41
+ ]
managers/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (1.09 kB). View file
 
managers/execution/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Execution subsystem - Python execution and monitoring.
3
+ """
4
+
5
+ from .python_executor import PythonExecutor
6
+ from .monitoring import Timing, TokenUsage
7
+
8
+ __all__ = ['PythonExecutor', 'Timing', 'TokenUsage']
managers/execution/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (464 Bytes). View file
 
managers/execution/__pycache__/monitoring.cpython-311.pyc ADDED
Binary file (2.54 kB). View file
 
managers/execution/__pycache__/python_executor.cpython-311.pyc ADDED
Binary file (7.6 kB). View file
 
managers/execution/monitoring.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Monitoring utilities for tracking execution timing and token usage.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ __all__ = ["Timing", "TokenUsage"]
8
+
9
+
10
+ @dataclass
11
+ class TokenUsage:
12
+ """
13
+ Contains the token usage information for a given step or run.
14
+ """
15
+
16
+ input_tokens: int
17
+ output_tokens: int
18
+ total_tokens: int = field(init=False)
19
+
20
+ def __post_init__(self):
21
+ self.total_tokens = self.input_tokens + self.output_tokens
22
+
23
+ def dict(self):
24
+ return {
25
+ "input_tokens": self.input_tokens,
26
+ "output_tokens": self.output_tokens,
27
+ "total_tokens": self.total_tokens,
28
+ }
29
+
30
+
31
+ @dataclass
32
+ class Timing:
33
+ """
34
+ Contains the timing information for a given step or run.
35
+ """
36
+
37
+ start_time: float
38
+ end_time: float | None = None
39
+
40
+ @property
41
+ def duration(self):
42
+ return None if self.end_time is None else self.end_time - self.start_time
43
+
44
+ def dict(self):
45
+ return {
46
+ "start_time": self.start_time,
47
+ "end_time": self.end_time,
48
+ "duration": self.duration,
49
+ }
50
+
51
+ def __repr__(self) -> str:
52
+ return f"Timing(start_time={self.start_time}, end_time={self.end_time}, duration={self.duration})"
managers/execution/python_executor.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Python Executor - Persistent Python execution environment.
3
+ Similar to Jupyter kernel or smolagents LocalPythonExecutor.
4
+ """
5
+
6
+ import io
7
+ import sys
8
+ from typing import Any, Dict
9
+
10
+ __all__ = ["PythonExecutor"]
11
+
12
+
13
+ class PythonExecutor:
14
+ """
15
+ Python execution environment similar to smolagents LocalPythonExecutor.
16
+ Maintains persistent state and tool namespace across executions.
17
+ """
18
+
19
+ def __init__(self):
20
+ self.namespace = {}
21
+ self.reset_environment()
22
+
23
+ def reset_environment(self):
24
+ """Reset the execution environment."""
25
+ # Start with basic Python builtins
26
+ self.namespace = {
27
+ "__builtins__": __builtins__,
28
+ }
29
+ # Add print output capture
30
+ self.namespace["_print_outputs"] = []
31
+
32
+ # Override print to capture outputs
33
+ def captured_print(*args, **kwargs):
34
+ output = " ".join(str(arg) for arg in args)
35
+ self.namespace["_print_outputs"].append(output)
36
+ print(*args, **kwargs) # Also print to console
37
+
38
+ self.namespace["print"] = captured_print
39
+
40
+ # Pre-import commonly used libraries
41
+ try:
42
+ import pandas as pd
43
+ import numpy as np
44
+ import os
45
+ from pathlib import Path
46
+ self.namespace["pd"] = pd
47
+ self.namespace["np"] = np
48
+ self.namespace["os"] = os
49
+ self.namespace["Path"] = Path
50
+
51
+ # Add file I/O helper functions
52
+ def write_text_file(filepath, content):
53
+ """Write text content to a file."""
54
+ with open(filepath, 'w') as f:
55
+ f.write(content)
56
+ return f"File written to {filepath}"
57
+
58
+ def write_dataframe_to_csv(df, filepath, **kwargs):
59
+ """Write pandas DataFrame to CSV file."""
60
+ df.to_csv(filepath, **kwargs)
61
+ return f"DataFrame written to {filepath}"
62
+
63
+ def write_dataframe_to_tsv(df, filepath, **kwargs):
64
+ """Write pandas DataFrame to TSV file."""
65
+ df.to_csv(filepath, sep='\t', **kwargs)
66
+ return f"DataFrame written to {filepath}"
67
+
68
+ def create_directory(dirpath):
69
+ """Create directory if it doesn't exist."""
70
+ Path(dirpath).mkdir(parents=True, exist_ok=True)
71
+ return f"Directory created: {dirpath}"
72
+
73
+ self.namespace["write_text_file"] = write_text_file
74
+ self.namespace["write_dataframe_to_csv"] = write_dataframe_to_csv
75
+ self.namespace["write_dataframe_to_tsv"] = write_dataframe_to_tsv
76
+ self.namespace["create_directory"] = create_directory
77
+
78
+ except ImportError as e:
79
+ print(f"Warning: Could not import library: {e}")
80
+
81
+ def send_functions(self, functions: Dict[str, Any]):
82
+ """Inject functions into the execution namespace."""
83
+ for func_name, func in functions.items():
84
+ self.namespace[func_name] = func
85
+
86
+ def send_tools(self, tools: Dict[str, Any]):
87
+ """Legacy method for backward compatibility - redirects to send_functions."""
88
+ self.send_functions(tools)
89
+
90
+ def send_variables(self, variables: Dict[str, Any]):
91
+ """Inject variables into the execution namespace."""
92
+ if variables:
93
+ print(f"📝 Injecting {len(variables)} variables into Python executor...")
94
+ self.namespace.update(variables)
95
+
96
+ def __call__(self, code: str) -> Any:
97
+ """Execute code in the persistent namespace."""
98
+ return self.execute(code)
99
+
100
+ def execute(self, code: str) -> Any:
101
+ """
102
+ Execute Python code in the persistent environment.
103
+ Similar to smolagents python execution.
104
+ """
105
+ try:
106
+ # Clear previous print outputs
107
+ self.namespace["_print_outputs"] = []
108
+
109
+ # Capture stdout
110
+ old_stdout = sys.stdout
111
+ sys.stdout = captured_output = io.StringIO()
112
+
113
+ try:
114
+ # Execute the code in our persistent namespace
115
+ exec(code, self.namespace)
116
+
117
+ # Get any stdout output
118
+ stdout_output = captured_output.getvalue()
119
+
120
+ # Get print outputs from our custom print function
121
+ print_outputs = self.namespace.get("_print_outputs", [])
122
+
123
+ # Combine outputs
124
+ all_outputs = []
125
+ if stdout_output.strip():
126
+ all_outputs.append(stdout_output.strip())
127
+ if print_outputs:
128
+ all_outputs.extend(print_outputs)
129
+
130
+ result = "\n".join(all_outputs) if all_outputs else "Code executed successfully"
131
+
132
+ return result
133
+
134
+ finally:
135
+ sys.stdout = old_stdout
136
+
137
+ except Exception as e:
138
+ return f"Error: {str(e)}"
managers/support/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Support subsystem - Console display and package management.
3
+ """
4
+
5
+ from .console_display import ConsoleDisplay
6
+ from .package_manager import PackageManager
7
+
8
+ __all__ = ['ConsoleDisplay', 'PackageManager']
managers/support/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (446 Bytes). View file
 
managers/support/__pycache__/console_display.cpython-311.pyc ADDED
Binary file (9.14 kB). View file
 
managers/support/__pycache__/package_manager.cpython-311.pyc ADDED
Binary file (3.76 kB). View file
 
managers/support/console_display.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Console Display Manager for CodeAct Agent.
3
+ Handles all console output and display formatting.
4
+ """
5
+
6
+ from typing import List, Dict
7
+ from rich import box
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.rule import Rule
11
+ from rich.syntax import Syntax
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+ from ..workflow.plan_manager import PlanManager
15
+
16
+
17
+ class ConsoleDisplay:
18
+ """Handles all console output and display formatting."""
19
+
20
+ def __init__(self):
21
+ self.console = Console()
22
+
23
+ def print_task_header(self, query: str):
24
+ """Print the task header panel."""
25
+ self.console.print(
26
+ Panel(
27
+ f"\n[bold]{query}\n",
28
+ title="[bold]Code Agent - New Run",
29
+ subtitle="Starting agent execution...",
30
+ border_style="yellow",
31
+ subtitle_align="left",
32
+ )
33
+ )
34
+
35
+ def print_tools_info(self, total_tools: int, regular_tools: List[str], mcp_tools: Dict):
36
+ """Print information about loaded tools."""
37
+ self.console.print(f"🛠️ Loaded {total_tools} total functions:")
38
+ if regular_tools:
39
+ self.console.print(f" 📋 Regular tools ({len(regular_tools)}): {regular_tools}")
40
+
41
+ if mcp_tools:
42
+ self.console.print(f" 🔗 MCP tools ({len(mcp_tools)}):")
43
+ for tool_name, tool_info in mcp_tools.items():
44
+ server_name = tool_info.get('server', 'unknown')
45
+ self.console.print(f" • {tool_name} (from {server_name} server)")
46
+
47
+ def print_packages_info(self, imported_packages: List[str], failed_packages: List[tuple]):
48
+ """Print information about imported packages."""
49
+ if imported_packages:
50
+ self.console.print(f"📦 Imported {len(imported_packages)} packages: {imported_packages}")
51
+
52
+ for package_name, error in failed_packages:
53
+ self.console.print(f"⚠️ Failed to import {package_name}: {error}")
54
+
55
+ def print_reasoning(self, content: str, step_count: int):
56
+ """Print agent reasoning panel."""
57
+ self.console.print(
58
+ Panel(
59
+ Text(content, style="italic"),
60
+ title=f"[bold]🧠 Agent Reasoning - Step {step_count}",
61
+ border_style="yellow",
62
+ box=box.SIMPLE,
63
+ )
64
+ )
65
+
66
+ def print_plan(self, plan: str):
67
+ """Print current plan panel with progress statistics."""
68
+ # Get plan progress statistics
69
+ progress = PlanManager.get_plan_progress(plan)
70
+ progress_text = f"({progress['completed']}/{progress['total']} completed)"
71
+
72
+ self.console.print(
73
+ Panel(
74
+ Text(plan, style="cyan"),
75
+ title=f"[bold]📋 Current Plan {progress_text}",
76
+ border_style="cyan",
77
+ box=box.SIMPLE,
78
+ )
79
+ )
80
+
81
+ def print_code_execution(self, code: str, step_count: int):
82
+ """Print code execution panel."""
83
+ self.console.print(f"⚡ Step {step_count}: Executing code...")
84
+ self.console.print(
85
+ Panel(
86
+ Syntax(code, "python", theme="monokai", word_wrap=True),
87
+ title=f"[bold]💻 Code Execution - Step {step_count}",
88
+ title_align="left",
89
+ box=box.HORIZONTALS,
90
+ )
91
+ )
92
+
93
+ def print_solution(self, solution: str, step_count: int):
94
+ """Print final solution panel."""
95
+ self.console.print(f"🎯 Step {step_count}: Providing final solution...")
96
+ self.console.print(
97
+ Panel(
98
+ Text(solution, style="bold green"),
99
+ title=f"[bold]✅ Final Solution - Step {step_count}",
100
+ border_style="green",
101
+ )
102
+ )
103
+
104
+ def print_error(self, error: str, step_count: int):
105
+ """Print error panel."""
106
+ self.console.print(f"❌ Step {step_count}: Error encountered...")
107
+ self.console.print(
108
+ Panel(
109
+ Text(error, style="bold red"),
110
+ title=f"[bold]⚠️ Error - Step {step_count}",
111
+ border_style="red",
112
+ )
113
+ )
114
+
115
+ def print_execution_result(self, result: str, step_count: int):
116
+ """Print execution result panel."""
117
+ self.console.print(
118
+ Panel(
119
+ Syntax(result, "text", theme="github-dark", word_wrap=True),
120
+ title=f"[bold]📊 Execution Result - Step {step_count}",
121
+ border_style="cyan",
122
+ title_align="left",
123
+ )
124
+ )
125
+
126
+ def print_execution_summary(self, step_count: int, error_count: int, duration: float):
127
+ """Print execution summary table."""
128
+ self.console.print(Rule(title="Execution Summary", style="yellow"))
129
+
130
+ summary_table = Table(show_header=False, box=box.SIMPLE)
131
+ summary_table.add_column("Metric", style="cyan")
132
+ summary_table.add_column("Value", style="bold")
133
+
134
+ summary_table.add_row("✅ Total steps", str(step_count))
135
+ summary_table.add_row("⚠️ Total errors", str(error_count))
136
+ summary_table.add_row("⏱️ Total execution time", f"{duration:.2f} seconds")
137
+ if step_count > 0:
138
+ summary_table.add_row("📊 Average time per step", f"{duration/step_count:.2f}s")
139
+
140
+ self.console.print(summary_table)
managers/support/package_manager.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Package Manager for CodeAct Agent.
3
+ Handles Python package management and imports.
4
+ """
5
+
6
+ from typing import Dict
7
+ from core.constants import LIBRARY_CONTENT_DICT
8
+
9
+
10
+ class PackageManager:
11
+ """Manages Python packages available to the agent."""
12
+
13
+ def __init__(self, default_packages: Dict[str, str] = None):
14
+ self.default_packages = default_packages or LIBRARY_CONTENT_DICT.copy()
15
+ self.custom_packages = {}
16
+
17
+ def add_packages(self, packages: Dict[str, str]) -> bool:
18
+ """Add new packages to the available packages."""
19
+ try:
20
+ if not isinstance(packages, dict):
21
+ raise ValueError("Packages must be a dictionary with package name as key and description as value")
22
+
23
+ for package_name, description in packages.items():
24
+ if not isinstance(package_name, str) or not isinstance(description, str):
25
+ print("Warning: Skipping invalid package entry - package_name and description must be strings")
26
+ continue
27
+
28
+ self.custom_packages[package_name] = description
29
+ print(f"Added package '{package_name}': {description}")
30
+
31
+ print(f"Successfully added {len(packages)} package(s) to the library")
32
+ return True
33
+
34
+ except Exception as e:
35
+ print(f"Error adding packages: {e}")
36
+ return False
37
+
38
+ def get_all_packages(self) -> Dict[str, str]:
39
+ """Get all available packages (default + custom)."""
40
+ all_packages = self.default_packages.copy()
41
+ all_packages.update(self.custom_packages)
42
+ return all_packages
43
+
44
+ def import_packages(self, python_executor) -> tuple:
45
+ """Import all packages into the executor environment."""
46
+ all_packages = self.get_all_packages()
47
+ imported_packages = []
48
+ failed_packages = []
49
+
50
+ for package_name in all_packages.keys():
51
+ try:
52
+ import_code = f"import {package_name}"
53
+ python_executor(import_code)
54
+ imported_packages.append(package_name)
55
+ except Exception as e:
56
+ failed_packages.append((package_name, str(e)))
57
+
58
+ return imported_packages, failed_packages
managers/tools/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tools subsystem - Tool management, registry, and integrations.
3
+ """
4
+
5
+ from .tool_manager import ToolManager, ToolSource, ToolInfo
6
+ from .tool_registry import ToolRegistry, tool, discover_tools_in_module, create_module2api_from_functions
7
+ from .tool_selector import ToolSelector
8
+ from .mcp_manager import MCPManager
9
+ from .builtin_tools import FUNCTION_REGISTRY, get_all_tool_functions
10
+
11
+ __all__ = [
12
+ 'ToolManager',
13
+ 'ToolSource',
14
+ 'ToolInfo',
15
+ 'ToolRegistry',
16
+ 'tool',
17
+ 'discover_tools_in_module',
18
+ 'create_module2api_from_functions',
19
+ 'ToolSelector',
20
+ 'MCPManager',
21
+ 'FUNCTION_REGISTRY',
22
+ 'get_all_tool_functions'
23
+ ]
managers/tools/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (891 Bytes). View file
 
managers/tools/__pycache__/builtin_tools.cpython-311.pyc ADDED
Binary file (1.24 kB). View file
 
managers/tools/__pycache__/mcp_manager.cpython-311.pyc ADDED
Binary file (31 kB). View file
 
managers/tools/__pycache__/tool_manager.cpython-311.pyc ADDED
Binary file (22.1 kB). View file
 
managers/tools/__pycache__/tool_registry.cpython-311.pyc ADDED
Binary file (16.3 kB). View file
 
managers/tools/__pycache__/tool_selector.cpython-311.pyc ADDED
Binary file (5.89 kB). View file
 
managers/tools/builtin_tools.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Built-in tool functions for CodeAct agent.
3
+ Example tools that demonstrate the tool system.
4
+ """
5
+
6
+ from typing import Any, List
7
+
8
+ __all__ = ["FUNCTION_REGISTRY", "get_all_tool_functions",
9
+ "add_numbers", "multiply_numbers"]
10
+
11
+
12
+ def add_numbers(a: int, b: int) -> int:
13
+ """Add two numbers together."""
14
+ return a + b
15
+
16
+
17
+ def multiply_numbers(a: int, b: int) -> int:
18
+ """Multiply two numbers together."""
19
+ return a * b
20
+
21
+
22
+ # ====================
23
+ # FUNCTION REGISTRY
24
+ # ====================
25
+
26
+ FUNCTION_REGISTRY = {
27
+ "add_numbers": add_numbers,
28
+ "multiply_numbers": multiply_numbers,
29
+ }
30
+
31
+
32
+ def get_all_tool_functions() -> List[Any]:
33
+ """Get all functions from FUNCTION_REGISTRY."""
34
+ return list(FUNCTION_REGISTRY.values())
managers/tools/mcp_manager.py ADDED
@@ -0,0 +1,588 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Manager for CodeAct Agent.
3
+ Manages MCP (Model Context Protocol) tools and servers.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import types
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Any
11
+ from rich.console import Console
12
+
13
+
14
+ class MCPManager:
15
+ """Manages MCP (Model Context Protocol) tools and servers."""
16
+
17
+ def __init__(self, console_display=None):
18
+ self.mcp_functions = {}
19
+ self.console = console_display.console if console_display else Console()
20
+
21
+ def has_mcp_functions(self) -> bool:
22
+ """Check if MCP functions are available."""
23
+ return bool(self.mcp_functions)
24
+
25
+ def group_tools_by_server(self, mcp_tools: Dict[str, dict]) -> Dict[str, List[Dict]]:
26
+ """Group MCP tools by server name."""
27
+ servers = {}
28
+ for tool_name, tool_info in mcp_tools.items():
29
+ server_name = tool_info.get('server', 'unknown')
30
+ if server_name not in servers:
31
+ servers[server_name] = []
32
+ servers[server_name].append({
33
+ 'name': tool_name,
34
+ 'description': tool_info.get('description', 'No description')
35
+ })
36
+ return servers
37
+
38
+ def add_mcp(self, config_path: str = "./mcp_config.yaml", tool_registry=None) -> None:
39
+ """Add MCP tools from configuration file."""
40
+ try:
41
+ import asyncio
42
+ import yaml
43
+ except ImportError as e:
44
+ raise ImportError(f"Required packages not available: {e}. Install with: pip install pyyaml") from e
45
+
46
+ try:
47
+ import nest_asyncio
48
+ from mcp import ClientSession
49
+ from mcp.client.stdio import StdioServerParameters, stdio_client
50
+ from mcp.client.streamable_http import streamablehttp_client
51
+ from langchain_mcp_adapters.tools import _list_all_tools
52
+ nest_asyncio.apply()
53
+ except ImportError as e:
54
+ raise ImportError(f"MCP packages not available: {e}. Install with: pip install mcp langchain-mcp-adapters") from e
55
+
56
+ def discover_mcp_tools_sync(server_params: StdioServerParameters) -> List[dict]:
57
+ """Discover available tools from MCP server synchronously."""
58
+ try:
59
+ async def _discover_async():
60
+ async with stdio_client(server_params) as (reader, writer):
61
+ async with ClientSession(reader, writer) as session:
62
+ await session.initialize()
63
+
64
+ tools_result = await session.list_tools()
65
+ tools = tools_result.tools if hasattr(tools_result, "tools") else tools_result
66
+ print(tools)
67
+
68
+ discovered_tools = []
69
+ for tool in tools:
70
+ if hasattr(tool, "name"):
71
+ # Ensure description is never empty or None
72
+ description = getattr(tool, 'description', None)
73
+ if not description or description.strip() == "":
74
+ # Generate description from tool name
75
+ formatted_name = tool.name.replace('_', ' ').title()
76
+ description = f"MCP tool: {formatted_name}"
77
+
78
+ discovered_tools.append({
79
+ "name": tool.name,
80
+ "description": description,
81
+ "inputSchema": tool.inputSchema,
82
+ })
83
+ else:
84
+ print(f"Warning: Skipping tool with no name attribute: {tool}")
85
+
86
+ return discovered_tools
87
+
88
+ return asyncio.run(_discover_async())
89
+ except Exception as e:
90
+ print(f"Failed to discover tools: {e}")
91
+ return []
92
+
93
+ def discover_remote_mcp_tools_sync(url: str) -> List[dict]:
94
+ """Discover available tools from remote MCP server synchronously."""
95
+ try:
96
+ async def _discover_remote_async():
97
+ async with streamablehttp_client(url) as (read, write, _):
98
+ async with ClientSession(read, write) as session:
99
+ await session.initialize()
100
+ tools = await _list_all_tools(session)
101
+
102
+ discovered_tools = []
103
+ for tool in tools:
104
+ if hasattr(tool, "name"):
105
+ # Ensure description is never empty or None
106
+ description = getattr(tool, 'description', None)
107
+ if not description or description.strip() == "":
108
+ # Generate description from tool name
109
+ formatted_name = tool.name.replace('_', ' ').title()
110
+ description = f"MCP tool: {formatted_name}"
111
+
112
+ discovered_tools.append({
113
+ "name": tool.name,
114
+ "description": description,
115
+ "inputSchema": tool.inputSchema,
116
+ })
117
+ else:
118
+ print(f"Warning: Skipping tool with no name attribute: {tool}")
119
+
120
+ return discovered_tools
121
+
122
+ return asyncio.run(_discover_remote_async())
123
+ except Exception as e:
124
+ print(f"Failed to discover remote tools from {url}: {e}")
125
+ return []
126
+
127
+ def make_mcp_wrapper(cmd: str, args: List[str], tool_name: str, doc: str, env_vars: dict = None):
128
+ """Create a synchronous wrapper for an async MCP tool call."""
129
+
130
+ def sync_tool_wrapper(**kwargs):
131
+ """Synchronous wrapper for MCP tool execution."""
132
+ try:
133
+ server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
134
+
135
+ async def async_tool_call():
136
+ async with stdio_client(server_params) as (reader, writer):
137
+ async with ClientSession(reader, writer) as session:
138
+ await session.initialize()
139
+ result = await session.call_tool(tool_name, kwargs)
140
+ content = result.content[0]
141
+ if hasattr(content, "model_dump_json"):
142
+ return content.model_dump_json()
143
+ elif hasattr(content, "json"):
144
+ return content.json()
145
+ return content.text
146
+
147
+ try:
148
+ loop = asyncio.get_running_loop()
149
+ return loop.create_task(async_tool_call())
150
+ except RuntimeError:
151
+ return asyncio.run(async_tool_call())
152
+
153
+ except Exception as e:
154
+ raise RuntimeError(f"MCP tool execution failed for '{tool_name}': {e}") from e
155
+
156
+ sync_tool_wrapper.__name__ = tool_name
157
+ sync_tool_wrapper.__doc__ = doc
158
+ return sync_tool_wrapper
159
+
160
+ def make_remote_mcp_wrapper(url: str, tool_name: str, doc: str):
161
+ """Create a synchronous wrapper for an async remote MCP tool call."""
162
+
163
+ def sync_tool_wrapper(**kwargs):
164
+ """Synchronous wrapper for remote MCP tool execution."""
165
+ try:
166
+ async def async_tool_call():
167
+ async with streamablehttp_client(url) as (read, write, _):
168
+ async with ClientSession(read, write) as session:
169
+ await session.initialize()
170
+ result = await session.call_tool(tool_name, kwargs)
171
+ content = result.content[0]
172
+ if hasattr(content, "model_dump_json"):
173
+ return content.model_dump_json()
174
+ elif hasattr(content, "json"):
175
+ return content.json()
176
+ return content.text
177
+
178
+ try:
179
+ loop = asyncio.get_running_loop()
180
+ return loop.create_task(async_tool_call())
181
+ except RuntimeError:
182
+ return asyncio.run(async_tool_call())
183
+
184
+ except Exception as e:
185
+ raise RuntimeError(f"Remote MCP tool execution failed for '{tool_name}': {e}") from e
186
+
187
+ sync_tool_wrapper.__name__ = tool_name
188
+ sync_tool_wrapper.__doc__ = doc
189
+ return sync_tool_wrapper
190
+
191
+ # Load and validate configuration
192
+ try:
193
+ config_content = Path(config_path).read_text(encoding="utf-8")
194
+ cfg = yaml.safe_load(config_content) or {}
195
+ except FileNotFoundError:
196
+ raise FileNotFoundError(f"MCP config file not found: {config_path}") from None
197
+ except yaml.YAMLError as e:
198
+ raise yaml.YAMLError(f"Invalid YAML in MCP config: {e}") from e
199
+
200
+ mcp_servers = cfg.get("mcp_servers", {})
201
+ if not mcp_servers:
202
+ print("Warning: No MCP servers found in configuration")
203
+ return
204
+
205
+ # Process each MCP server configuration
206
+ for server_name, server_meta in mcp_servers.items():
207
+ if not server_meta.get("enabled", True):
208
+ continue
209
+
210
+ # Check if this is a remote server configuration
211
+ remote_url = server_meta.get("url")
212
+ if remote_url:
213
+ # Handle remote MCP server
214
+ self._process_remote_server(server_name, server_meta, remote_url, tool_registry, discover_remote_mcp_tools_sync, make_remote_mcp_wrapper)
215
+ continue
216
+
217
+ # Handle local MCP server (existing logic)
218
+ # Validate command configuration
219
+ cmd_list = server_meta.get("command", [])
220
+ if not cmd_list or not isinstance(cmd_list, list):
221
+ print(f"Warning: Invalid command configuration for server '{server_name}'")
222
+ continue
223
+
224
+ cmd, *args = cmd_list
225
+
226
+ # Process environment variables
227
+ env_vars = server_meta.get("env", {})
228
+ if env_vars:
229
+ processed_env = {}
230
+ for key, value in env_vars.items():
231
+ if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
232
+ var_name = value[2:-1]
233
+ processed_env[key] = os.getenv(var_name, "")
234
+ else:
235
+ processed_env[key] = value
236
+ env_vars = processed_env
237
+
238
+ # Create module namespace for this MCP server
239
+ mcp_module_name = f"mcp_servers.{server_name}"
240
+ if mcp_module_name not in sys.modules:
241
+ sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
242
+ server_module = sys.modules[mcp_module_name]
243
+
244
+ tools_config = server_meta.get("tools", [])
245
+
246
+ # Auto-discover tools if not manually configured
247
+ if not tools_config:
248
+ try:
249
+ server_params = StdioServerParameters(command=cmd, args=args, env=env_vars)
250
+ tools_config = discover_mcp_tools_sync(server_params)
251
+
252
+ if tools_config:
253
+ print(f"🔍 Discovered {len(tools_config)} tools from {server_name} MCP server")
254
+ else:
255
+ print(f"Warning: No tools discovered from {server_name} MCP server")
256
+ continue
257
+
258
+ except Exception as e:
259
+ print(f"Failed to discover tools for {server_name}: {e}")
260
+ continue
261
+
262
+ # Register each tool
263
+ tools_added = 0
264
+ for tool_meta in tools_config:
265
+ if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
266
+ # Manual tool definition (Biomni-style)
267
+ tool_name = tool_meta.get("biomni_name")
268
+ description = tool_meta.get("description", f"MCP tool: {tool_name}")
269
+ parameters = tool_meta.get("parameters", {})
270
+ required_param_names = []
271
+ for param_name, param_spec in parameters.items():
272
+ if param_spec.get("required", False):
273
+ required_param_names.append(param_name)
274
+ else:
275
+ # Auto-discovered tool
276
+ tool_name = tool_meta.get("name")
277
+ description = tool_meta.get("description", "")
278
+
279
+ # Ensure description is never empty
280
+ if not description or description.strip() == "":
281
+ formatted_name = tool_name.replace('_', ' ').title()
282
+ description = f"MCP tool: {formatted_name}"
283
+
284
+ input_schema = tool_meta.get("inputSchema", {})
285
+ parameters = input_schema.get("properties", {})
286
+ required_param_names = input_schema.get("required", [])
287
+
288
+ if not tool_name:
289
+ print(f"Warning: Skipping tool with no name in {server_name}")
290
+ continue
291
+
292
+ # Create wrapper function
293
+ wrapper_function = make_mcp_wrapper(cmd, args, tool_name, description, env_vars)
294
+
295
+ # Add to module namespace
296
+ setattr(server_module, tool_name, wrapper_function)
297
+
298
+ # Store in MCP functions registry with parameter information
299
+ self.mcp_functions[tool_name] = {
300
+ "function": wrapper_function,
301
+ "server": server_name,
302
+ "module": mcp_module_name,
303
+ "description": description,
304
+ "required_parameters": [], # Will be populated below
305
+ "optional_parameters": [] # Will be populated below
306
+ }
307
+
308
+ # Register with tool registry if available
309
+ if tool_registry:
310
+ from .tool_registry import ToolRegistry
311
+ # Create tool schema with proper parameter information
312
+ required_params = []
313
+ optional_params = []
314
+
315
+ for param_name, param_spec in parameters.items():
316
+ param_info = {
317
+ "name": param_name,
318
+ "type": param_spec.get("type", "string"),
319
+ "description": param_spec.get("description", f"Parameter {param_name}"),
320
+ }
321
+
322
+ # Extract enum/literal values if present
323
+ if "enum" in param_spec:
324
+ param_info["enum"] = param_spec["enum"]
325
+
326
+ # Handle anyOf schemas (common for optional literal types)
327
+ if "anyOf" in param_spec:
328
+ # Look for enum in anyOf schemas
329
+ for schema_option in param_spec["anyOf"]:
330
+ if "enum" in schema_option:
331
+ param_info["enum"] = schema_option["enum"]
332
+ # Update type if specified
333
+ if "type" in schema_option:
334
+ param_info["type"] = schema_option["type"]
335
+ break
336
+
337
+ # Handle oneOf schemas (alternative union syntax)
338
+ if "oneOf" in param_spec:
339
+ # Look for enum in oneOf schemas
340
+ for schema_option in param_spec["oneOf"]:
341
+ if "enum" in schema_option:
342
+ param_info["enum"] = schema_option["enum"]
343
+ if "type" in schema_option:
344
+ param_info["type"] = schema_option["type"]
345
+ break
346
+
347
+ # Determine if parameter is required based on:
348
+ # 1. Explicit required list (if provided)
349
+ # 2. If no default value is present in the schema
350
+ is_required = (param_name in required_param_names) or ("default" not in param_spec)
351
+
352
+ if is_required:
353
+ required_params.append(param_info)
354
+ else:
355
+ param_info["default"] = param_spec.get("default")
356
+ optional_params.append(param_info)
357
+
358
+ # Create complete tool schema
359
+ tool_schema = {
360
+ "name": tool_name,
361
+ "description": description,
362
+ "required_parameters": required_params,
363
+ "optional_parameters": optional_params,
364
+ "module": mcp_module_name,
365
+ }
366
+
367
+ success = tool_registry.register_tool(tool_schema, mcp_module_name)
368
+ if success:
369
+ tool_registry._name_to_function[tool_name] = wrapper_function
370
+ tools_added += 1
371
+
372
+ # Update MCP functions registry with parameter information
373
+ self.mcp_functions[tool_name]["required_parameters"] = required_params
374
+ self.mcp_functions[tool_name]["optional_parameters"] = optional_params
375
+
376
+ if tools_added > 0:
377
+ print(f"✅ Added {tools_added} MCP tools from {server_name} server")
378
+
379
+ print(f"🛠️ Total MCP tools loaded: {len(self.mcp_functions)}")
380
+
381
+ def _process_remote_server(self, server_name: str, server_meta: dict, remote_url: str, tool_registry, discover_remote_mcp_tools_sync, make_remote_mcp_wrapper):
382
+ """Process a remote MCP server configuration."""
383
+ import sys
384
+ import types
385
+
386
+ # Create module namespace for this remote MCP server
387
+ mcp_module_name = f"mcp_servers.{server_name}"
388
+ if mcp_module_name not in sys.modules:
389
+ sys.modules[mcp_module_name] = types.ModuleType(mcp_module_name)
390
+ server_module = sys.modules[mcp_module_name]
391
+
392
+ tools_config = server_meta.get("tools", [])
393
+
394
+ # Auto-discover tools if not manually configured
395
+ if not tools_config:
396
+ try:
397
+ tools_config = discover_remote_mcp_tools_sync(remote_url)
398
+
399
+ if tools_config:
400
+ print(f"🔍 Discovered {len(tools_config)} tools from {server_name} remote MCP server")
401
+ else:
402
+ print(f"Warning: No tools discovered from {server_name} remote MCP server")
403
+ return
404
+
405
+ except Exception as e:
406
+ print(f"Failed to discover tools for remote {server_name}: {e}")
407
+ return
408
+
409
+ # Register each tool
410
+ tools_added = 0
411
+ for tool_meta in tools_config:
412
+ if isinstance(tool_meta, dict) and "biomni_name" in tool_meta:
413
+ # Manual tool definition (Biomni-style)
414
+ tool_name = tool_meta.get("biomni_name")
415
+ description = tool_meta.get("description", f"Remote MCP tool: {tool_name}")
416
+ parameters = tool_meta.get("parameters", {})
417
+ required_param_names = []
418
+ for param_name, param_spec in parameters.items():
419
+ if param_spec.get("required", False):
420
+ required_param_names.append(param_name)
421
+ else:
422
+ # Auto-discovered tool
423
+ tool_name = tool_meta.get("name")
424
+ description = tool_meta.get("description", "")
425
+
426
+ # Ensure description is never empty
427
+ if not description or description.strip() == "":
428
+ formatted_name = tool_name.replace('_', ' ').title()
429
+ description = f"Remote MCP tool: {formatted_name}"
430
+
431
+ input_schema = tool_meta.get("inputSchema", {})
432
+ parameters = input_schema.get("properties", {})
433
+ required_param_names = input_schema.get("required", [])
434
+
435
+ if not tool_name:
436
+ print(f"Warning: Skipping tool with no name in remote {server_name}")
437
+ continue
438
+
439
+ # Create wrapper function for remote tool
440
+ wrapper_function = make_remote_mcp_wrapper(remote_url, tool_name, description)
441
+
442
+ # Add to module namespace
443
+ setattr(server_module, tool_name, wrapper_function)
444
+
445
+ # Store in MCP functions registry with parameter information
446
+ self.mcp_functions[tool_name] = {
447
+ "function": wrapper_function,
448
+ "server": server_name,
449
+ "module": mcp_module_name,
450
+ "description": description,
451
+ "required_parameters": [], # Will be populated below
452
+ "optional_parameters": [], # Will be populated below
453
+ "remote_url": remote_url
454
+ }
455
+
456
+ # Register with tool registry if available
457
+ if tool_registry:
458
+ from .tool_registry import ToolRegistry
459
+ # Create tool schema with proper parameter information
460
+ required_params = []
461
+ optional_params = []
462
+
463
+ for param_name, param_spec in parameters.items():
464
+ param_info = {
465
+ "name": param_name,
466
+ "type": param_spec.get("type", "string"),
467
+ "description": param_spec.get("description", f"Parameter {param_name}"),
468
+ }
469
+
470
+ # Extract enum/literal values if present
471
+ if "enum" in param_spec:
472
+ param_info["enum"] = param_spec["enum"]
473
+
474
+ # Handle anyOf schemas (common for optional literal types)
475
+ if "anyOf" in param_spec:
476
+ # Look for enum in anyOf schemas
477
+ for schema_option in param_spec["anyOf"]:
478
+ if "enum" in schema_option:
479
+ param_info["enum"] = schema_option["enum"]
480
+ # Update type if specified
481
+ if "type" in schema_option:
482
+ param_info["type"] = schema_option["type"]
483
+ break
484
+
485
+ # Handle oneOf schemas (alternative union syntax)
486
+ if "oneOf" in param_spec:
487
+ # Look for enum in oneOf schemas
488
+ for schema_option in param_spec["oneOf"]:
489
+ if "enum" in schema_option:
490
+ param_info["enum"] = schema_option["enum"]
491
+ if "type" in schema_option:
492
+ param_info["type"] = schema_option["type"]
493
+ break
494
+
495
+ # Determine if parameter is required based on:
496
+ # 1. Explicit required list (if provided)
497
+ # 2. If no default value is present in the schema
498
+ is_required = (param_name in required_param_names) or ("default" not in param_spec)
499
+
500
+ if is_required:
501
+ required_params.append(param_info)
502
+ else:
503
+ param_info["default"] = param_spec.get("default")
504
+ optional_params.append(param_info)
505
+
506
+ # Create complete tool schema
507
+ tool_schema = {
508
+ "name": tool_name,
509
+ "description": description,
510
+ "required_parameters": required_params,
511
+ "optional_parameters": optional_params,
512
+ "module": mcp_module_name,
513
+ }
514
+
515
+ success = tool_registry.register_tool(tool_schema, mcp_module_name)
516
+ if success:
517
+ tool_registry._name_to_function[tool_name] = wrapper_function
518
+ tools_added += 1
519
+
520
+ # Update MCP functions registry with parameter information
521
+ self.mcp_functions[tool_name]["required_parameters"] = required_params
522
+ self.mcp_functions[tool_name]["optional_parameters"] = optional_params
523
+
524
+ if tools_added > 0:
525
+ print(f"✅ Added {tools_added} remote MCP tools from {server_name} server")
526
+
527
+ def list_mcp_tools(self) -> Dict[str, dict]:
528
+ """List all loaded MCP tools."""
529
+ return self.mcp_functions.copy()
530
+
531
+ def remove_mcp_tool(self, tool_name: str, tool_registry=None) -> bool:
532
+ """Remove an MCP tool by name."""
533
+ if not self.has_mcp_functions() or tool_name not in self.mcp_functions:
534
+ return False
535
+
536
+ # Remove from tool registry
537
+ if tool_registry:
538
+ tool_registry.remove_tool_by_name(tool_name)
539
+
540
+ # Remove from MCP functions
541
+ del self.mcp_functions[tool_name]
542
+ return True
543
+
544
+ def show_mcp_status(self) -> None:
545
+ """Display detailed MCP status information to the user."""
546
+ if not self.has_mcp_functions():
547
+ self.console.print("🔗 No MCP tools loaded")
548
+ return
549
+
550
+ mcp_tools = self.mcp_functions
551
+ if not mcp_tools:
552
+ self.console.print("🔗 MCP system initialized but no tools loaded")
553
+ return
554
+
555
+ # Group tools by server
556
+ servers = self.group_tools_by_server(mcp_tools)
557
+
558
+ # Display server information
559
+ self.console.print(f"\n🔗 MCP Status Report:")
560
+ self.console.print(f" 📊 Total servers: {len(servers)}")
561
+ self.console.print(f" 🛠️ Total MCP tools: {len(mcp_tools)}")
562
+
563
+ for server_name, tools in servers.items():
564
+ self.console.print(f"\n 📡 Server: {server_name}")
565
+ self.console.print(f" Status: ✅ Active ({len(tools)} tools)")
566
+ for tool in tools:
567
+ self.console.print(f" • {tool['name']}: {tool['description']}")
568
+
569
+ def get_mcp_summary(self) -> Dict[str, any]:
570
+ """Get a summary of MCP tools for programmatic access."""
571
+ if not self.has_mcp_functions():
572
+ return {"total_tools": 0, "servers": {}, "tools": {}}
573
+
574
+ mcp_tools = self.mcp_functions
575
+ # Group tools by server but only get tool names
576
+ servers = {}
577
+ for tool_name, tool_info in mcp_tools.items():
578
+ server_name = tool_info.get('server', 'unknown')
579
+ if server_name not in servers:
580
+ servers[server_name] = []
581
+ servers[server_name].append(tool_name)
582
+
583
+ return {
584
+ "total_tools": len(mcp_tools),
585
+ "total_servers": len(servers),
586
+ "servers": servers,
587
+ "tools": {name: info.get('description', '') for name, info in mcp_tools.items()}
588
+ }
managers/tools/tool_manager.py ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tool Manager for CodeAct Agent.
3
+ Unified management system for all types of tools: local functions, decorated tools, and MCP tools.
4
+ """
5
+
6
+ from typing import Dict, List, Optional, Callable, Any, Union
7
+ from enum import Enum
8
+ from dataclasses import dataclass
9
+ import re
10
+
11
+ # Import components
12
+ from .tool_registry import ToolRegistry, create_module2api_from_functions
13
+ from .mcp_manager import MCPManager
14
+
15
+
16
+ class ToolSource(Enum):
17
+ """Enumeration of tool sources."""
18
+ LOCAL = "local"
19
+ DECORATED = "decorated"
20
+ MCP = "mcp"
21
+ ALL = "all"
22
+
23
+
24
+ @dataclass
25
+ class ToolInfo:
26
+ """Comprehensive tool information."""
27
+ name: str
28
+ description: str
29
+ source: ToolSource
30
+ function: Optional[Callable] = None
31
+ schema: Optional[Dict] = None
32
+ server: Optional[str] = None # For MCP tools
33
+ module: Optional[str] = None
34
+ required_parameters: List[Dict] = None
35
+ optional_parameters: List[Dict] = None
36
+
37
+ def __post_init__(self):
38
+ if self.required_parameters is None:
39
+ self.required_parameters = []
40
+ if self.optional_parameters is None:
41
+ self.optional_parameters = []
42
+
43
+
44
+ class ToolManager:
45
+ """
46
+ Unified tool management system for CodeAct Agent.
47
+
48
+ Manages all types of tools:
49
+ - Local functions (legacy function registry)
50
+ - Decorated tools (@tool decorator)
51
+ - MCP tools (Model Context Protocol)
52
+
53
+ Provides a single, consistent interface for tool operations.
54
+ """
55
+
56
+ def __init__(self, console_display=None):
57
+ """
58
+ Initialize the ToolManager.
59
+
60
+ Args:
61
+ console_display: Optional console display for MCP status output
62
+ """
63
+ # Core components
64
+ self.tool_registry = ToolRegistry()
65
+ self.mcp_manager = MCPManager(console_display)
66
+
67
+ # Unified tool catalog
68
+ self._tool_catalog: Dict[str, ToolInfo] = {}
69
+
70
+ # Legacy function registry (for backward compatibility)
71
+ self._legacy_functions: Dict[str, Callable] = {}
72
+
73
+ # Initialize with decorated tools from function_tools.py
74
+ self._discover_decorated_tools()
75
+
76
+ # ====================
77
+ # CORE TOOL MANAGEMENT
78
+ # ====================
79
+
80
+ def add_tool(self, tool: Union[Callable, Dict], name: str = None,
81
+ description: str = None, source: ToolSource = ToolSource.LOCAL) -> bool:
82
+ """
83
+ Add a tool to the manager.
84
+
85
+ Args:
86
+ tool: Either a callable function or a tool schema dict
87
+ name: Optional custom name (defaults to function.__name__)
88
+ description: Optional description (defaults to function.__doc__)
89
+ source: Source type of the tool
90
+
91
+ Returns:
92
+ True if successfully added
93
+ """
94
+ try:
95
+ if callable(tool):
96
+ # Handle callable functions
97
+ tool_name = name or tool.__name__
98
+ tool_desc = description or tool.__doc__ or f"Function {tool.__name__}"
99
+
100
+ # Add to tool registry
101
+ success = self.tool_registry.add_function_directly(tool_name, tool, tool_desc)
102
+
103
+ if success:
104
+ # Create ToolInfo and add to catalog
105
+ tool_info = ToolInfo(
106
+ name=tool_name,
107
+ description=tool_desc,
108
+ source=source,
109
+ function=tool,
110
+ schema=self._create_schema_from_function(tool_name, tool, tool_desc)
111
+ )
112
+ self._tool_catalog[tool_name] = tool_info
113
+
114
+ if source == ToolSource.LOCAL:
115
+ self._legacy_functions[tool_name] = tool
116
+
117
+ return True
118
+
119
+ elif isinstance(tool, dict):
120
+ # Handle tool schema dictionaries
121
+ tool_name = tool.get("name") or name
122
+ tool_desc = tool.get("description") or description
123
+
124
+ if not tool_name:
125
+ print("Warning: Tool schema must have a name")
126
+ return False
127
+
128
+ # Register with tool registry
129
+ success = self.tool_registry.register_tool(tool)
130
+
131
+ if success:
132
+ tool_info = ToolInfo(
133
+ name=tool_name,
134
+ description=tool_desc,
135
+ source=source,
136
+ schema=tool,
137
+ required_parameters=tool.get("required_parameters", []),
138
+ optional_parameters=tool.get("optional_parameters", [])
139
+ )
140
+ self._tool_catalog[tool_name] = tool_info
141
+ return True
142
+
143
+ return False
144
+
145
+ except Exception as e:
146
+ print(f"Error adding tool {name}: {e}")
147
+ return False
148
+
149
+ def remove_tool(self, name: str) -> bool:
150
+ """Remove a tool by name from all registries."""
151
+ try:
152
+ success = False
153
+
154
+ # Remove from tool registry
155
+ if self.tool_registry.remove_tool_by_name(name):
156
+ success = True
157
+
158
+ # Remove from MCP if it's an MCP tool
159
+ if name in self.mcp_manager.mcp_functions:
160
+ if self.mcp_manager.remove_mcp_tool(name, self.tool_registry):
161
+ success = True
162
+
163
+ # Remove from legacy functions
164
+ if name in self._legacy_functions:
165
+ del self._legacy_functions[name]
166
+ success = True
167
+
168
+ # Remove from catalog
169
+ if name in self._tool_catalog:
170
+ del self._tool_catalog[name]
171
+ success = True
172
+
173
+ return success
174
+
175
+ except Exception as e:
176
+ print(f"Error removing tool {name}: {e}")
177
+ return False
178
+
179
+ def get_tool(self, name: str) -> Optional[ToolInfo]:
180
+ """Get comprehensive tool information by name."""
181
+ return self._tool_catalog.get(name)
182
+
183
+ def get_tool_function(self, name: str) -> Optional[Callable]:
184
+ """Get the actual function object by name."""
185
+ tool_info = self.get_tool(name)
186
+ if tool_info and tool_info.function:
187
+ return tool_info.function
188
+
189
+ # Check tool registry
190
+ return self.tool_registry.get_function_by_name(name)
191
+
192
+ # ====================
193
+ # TOOL DISCOVERY AND LISTING
194
+ # ====================
195
+
196
+ def list_tools(self, source: ToolSource = ToolSource.ALL,
197
+ include_details: bool = False) -> List[Dict]:
198
+ """
199
+ List tools with optional filtering by source.
200
+
201
+ Args:
202
+ source: Filter by tool source (LOCAL, DECORATED, MCP, ALL)
203
+ include_details: Whether to include detailed information
204
+
205
+ Returns:
206
+ List of tool dictionaries
207
+ """
208
+ tools = []
209
+
210
+ for tool_name, tool_info in self._tool_catalog.items():
211
+ if source == ToolSource.ALL or tool_info.source == source:
212
+ if include_details:
213
+ tools.append({
214
+ "name": tool_info.name,
215
+ "description": tool_info.description,
216
+ "source": tool_info.source.value,
217
+ "server": tool_info.server,
218
+ "module": tool_info.module,
219
+ "has_function": tool_info.function is not None,
220
+ "required_params": len(tool_info.required_parameters),
221
+ "optional_params": len(tool_info.optional_parameters)
222
+ })
223
+ else:
224
+ tools.append({
225
+ "name": tool_info.name,
226
+ "description": tool_info.description,
227
+ "source": tool_info.source.value
228
+ })
229
+
230
+ return sorted(tools, key=lambda x: x["name"])
231
+
232
+ def search_tools(self, query: str, search_descriptions: bool = True) -> List[Dict]:
233
+ """
234
+ Search tools by name and optionally description.
235
+
236
+ Args:
237
+ query: Search query (supports regex)
238
+ search_descriptions: Whether to also search in descriptions
239
+
240
+ Returns:
241
+ List of matching tools
242
+ """
243
+ pattern = re.compile(query, re.IGNORECASE)
244
+ matching_tools = []
245
+
246
+ for tool_name, tool_info in self._tool_catalog.items():
247
+ match = False
248
+
249
+ # Search in name
250
+ if pattern.search(tool_name):
251
+ match = True
252
+
253
+ # Search in description if enabled
254
+ elif search_descriptions and pattern.search(tool_info.description or ""):
255
+ match = True
256
+
257
+ if match:
258
+ matching_tools.append({
259
+ "name": tool_info.name,
260
+ "description": tool_info.description,
261
+ "source": tool_info.source.value,
262
+ "server": tool_info.server
263
+ })
264
+
265
+ return sorted(matching_tools, key=lambda x: x["name"])
266
+
267
+ def get_tools_by_source(self, source: ToolSource) -> Dict[str, ToolInfo]:
268
+ """Get all tools from a specific source."""
269
+ return {
270
+ name: tool_info
271
+ for name, tool_info in self._tool_catalog.items()
272
+ if tool_info.source == source
273
+ }
274
+
275
+ # ====================
276
+ # MCP INTEGRATION
277
+ # ====================
278
+
279
+ def add_mcp_server(self, config_path: str = "./mcp_config.yaml") -> None:
280
+ """Add MCP tools from configuration file."""
281
+ try:
282
+ # Use MCP manager to load tools
283
+ self.mcp_manager.add_mcp(config_path, self.tool_registry)
284
+
285
+ # Update our catalog with MCP tools
286
+ mcp_tools = self.mcp_manager.list_mcp_tools()
287
+ for tool_name, tool_data in mcp_tools.items():
288
+ tool_info = ToolInfo(
289
+ name=tool_name,
290
+ description=tool_data.get("description", "MCP tool"),
291
+ source=ToolSource.MCP,
292
+ function=tool_data.get("function"),
293
+ server=tool_data.get("server"),
294
+ module=tool_data.get("module"),
295
+ required_parameters=tool_data.get("required_parameters", []),
296
+ optional_parameters=tool_data.get("optional_parameters", [])
297
+ )
298
+ self._tool_catalog[tool_name] = tool_info
299
+
300
+ except Exception as e:
301
+ print(f"Error adding MCP server: {e}")
302
+
303
+ def list_mcp_servers(self) -> Dict[str, List[str]]:
304
+ """List all MCP servers and their tools."""
305
+ mcp_tools = self.get_tools_by_source(ToolSource.MCP)
306
+ servers = {}
307
+
308
+ for tool_name, tool_info in mcp_tools.items():
309
+ server_name = tool_info.server or "unknown"
310
+ if server_name not in servers:
311
+ servers[server_name] = []
312
+ servers[server_name].append(tool_name)
313
+
314
+ return servers
315
+
316
+ def show_mcp_status(self) -> None:
317
+ """Display detailed MCP status."""
318
+ self.mcp_manager.show_mcp_status()
319
+
320
+ def get_mcp_summary(self) -> Dict[str, Any]:
321
+ """Get MCP tools summary."""
322
+ return self.mcp_manager.get_mcp_summary()
323
+
324
+ # ====================
325
+ # TOOL EXECUTION SUPPORT
326
+ # ====================
327
+
328
+ def get_all_functions(self) -> Dict[str, Callable]:
329
+ """Get all available functions as a dictionary."""
330
+ functions = {}
331
+
332
+ # Add from tool registry
333
+ functions.update(self.tool_registry.get_all_functions())
334
+
335
+ # Add from legacy functions
336
+ functions.update(self._legacy_functions)
337
+
338
+ # Add MCP functions
339
+ mcp_tools = self.mcp_manager.list_mcp_tools()
340
+ for tool_name, tool_data in mcp_tools.items():
341
+ if tool_data.get("function"):
342
+ functions[tool_name] = tool_data["function"]
343
+
344
+ return functions
345
+
346
+ def get_tool_schemas(self, openai_format: bool = True) -> List[Dict]:
347
+ """
348
+ Get tool schemas for all tools.
349
+
350
+ Args:
351
+ openai_format: Whether to format as OpenAI function schemas
352
+
353
+ Returns:
354
+ List of tool schemas
355
+ """
356
+ schemas = []
357
+
358
+ for tool_name, tool_info in self._tool_catalog.items():
359
+ if openai_format:
360
+ # Convert to OpenAI function schema format
361
+ schema = {
362
+ "type": "function",
363
+ "function": {
364
+ "name": tool_info.name,
365
+ "description": tool_info.description,
366
+ "parameters": {
367
+ "type": "object",
368
+ "properties": {},
369
+ "required": []
370
+ }
371
+ }
372
+ }
373
+
374
+ # Add required parameters
375
+ for param in tool_info.required_parameters:
376
+ param_schema = {
377
+ "type": param.get("type", "string"),
378
+ "description": param.get("description", "")
379
+ }
380
+ # Add enum values if present
381
+ if "enum" in param:
382
+ param_schema["enum"] = param["enum"]
383
+ schema["function"]["parameters"]["properties"][param["name"]] = param_schema
384
+ schema["function"]["parameters"]["required"].append(param["name"])
385
+
386
+ # Add optional parameters
387
+ for param in tool_info.optional_parameters:
388
+ param_schema = {
389
+ "type": param.get("type", "string"),
390
+ "description": param.get("description", "")
391
+ }
392
+ # Add enum values if present
393
+ if "enum" in param:
394
+ param_schema["enum"] = param["enum"]
395
+ if "default" in param:
396
+ param_schema["default"] = param["default"]
397
+ schema["function"]["parameters"]["properties"][param["name"]] = param_schema
398
+
399
+ schemas.append(schema)
400
+ else:
401
+ # Return raw schema
402
+ if tool_info.schema:
403
+ schemas.append(tool_info.schema)
404
+
405
+ return schemas
406
+
407
+ # ====================
408
+ # STATISTICS AND REPORTING
409
+ # ====================
410
+
411
+ def get_tool_statistics(self) -> Dict[str, Any]:
412
+ """Get comprehensive tool statistics."""
413
+ stats = {
414
+ "total_tools": len(self._tool_catalog),
415
+ "by_source": {source.value: 0 for source in ToolSource if source != ToolSource.ALL},
416
+ "with_functions": 0,
417
+ "mcp_servers": len(self.list_mcp_servers()),
418
+ "tool_registry_size": len(self.tool_registry.tools),
419
+ "legacy_functions": len(self._legacy_functions)
420
+ }
421
+
422
+ for tool_info in self._tool_catalog.values():
423
+ stats["by_source"][tool_info.source.value] += 1
424
+ if tool_info.function:
425
+ stats["with_functions"] += 1
426
+
427
+ return stats
428
+
429
+ def validate_tools(self) -> Dict[str, List[str]]:
430
+ """Validate all tools and return any issues found."""
431
+ issues = {
432
+ "missing_functions": [],
433
+ "missing_descriptions": [],
434
+ "duplicate_names": [],
435
+ "invalid_schemas": []
436
+ }
437
+
438
+ seen_names = set()
439
+ for tool_name, tool_info in self._tool_catalog.items():
440
+ # Check for duplicates
441
+ if tool_name in seen_names:
442
+ issues["duplicate_names"].append(tool_name)
443
+ seen_names.add(tool_name)
444
+
445
+ # Check for missing functions (except MCP tools which may not have direct functions)
446
+ if not tool_info.function and tool_info.source != ToolSource.MCP:
447
+ issues["missing_functions"].append(tool_name)
448
+
449
+ # Check for missing descriptions
450
+ if not tool_info.description or tool_info.description.strip() == "":
451
+ issues["missing_descriptions"].append(tool_name)
452
+
453
+ return issues
454
+
455
+ # ====================
456
+ # PRIVATE METHODS
457
+ # ====================
458
+
459
+ def _discover_decorated_tools(self) -> None:
460
+ """Discover and register tools marked with @tool decorator."""
461
+ try:
462
+ from .builtin_tools import get_all_tool_functions
463
+
464
+ tool_functions = get_all_tool_functions()
465
+ for func in tool_functions:
466
+ name = getattr(func, '_tool_name', func.__name__)
467
+ description = getattr(func, '_tool_description', func.__doc__ or f"Function {func.__name__}")
468
+
469
+ tool_info = ToolInfo(
470
+ name=name,
471
+ description=description,
472
+ source=ToolSource.DECORATED,
473
+ function=func,
474
+ schema=self._create_schema_from_function(name, func, description)
475
+ )
476
+ self._tool_catalog[name] = tool_info
477
+
478
+ # Also add to tool registry for consistency
479
+ self.tool_registry.add_function_directly(name, func, description)
480
+
481
+ except ImportError:
482
+ print("Warning: Could not import builtin_tools module for decorated tool discovery")
483
+
484
+ def _create_schema_from_function(self, name: str, function: Callable, description: str) -> Dict:
485
+ """Create a tool schema from a function object."""
486
+ return self.tool_registry._create_schema_from_function(name, function, description)
487
+
488
+ def _refresh_catalog(self) -> None:
489
+ """Refresh the tool catalog from all sources."""
490
+ # Clear current catalog
491
+ self._tool_catalog.clear()
492
+
493
+ # Re-discover decorated tools
494
+ self._discover_decorated_tools()
495
+
496
+ # Re-add MCP tools
497
+ mcp_tools = self.mcp_manager.list_mcp_tools()
498
+ for tool_name, tool_data in mcp_tools.items():
499
+ tool_info = ToolInfo(
500
+ name=tool_name,
501
+ description=tool_data.get("description", "MCP tool"),
502
+ source=ToolSource.MCP,
503
+ function=tool_data.get("function"),
504
+ server=tool_data.get("server"),
505
+ module=tool_data.get("module"),
506
+ required_parameters=tool_data.get("required_parameters", []),
507
+ optional_parameters=tool_data.get("optional_parameters", [])
508
+ )
509
+ self._tool_catalog[tool_name] = tool_info
510
+
511
+ # ====================
512
+ # LEGACY COMPATIBILITY
513
+ # ====================
514
+
515
+ def add_legacy_functions(self, functions: Dict[str, Callable]) -> int:
516
+ """Add legacy functions for backward compatibility."""
517
+ added_count = 0
518
+ for name, func in functions.items():
519
+ if self.add_tool(func, name, source=ToolSource.LOCAL):
520
+ added_count += 1
521
+ return added_count
managers/tools/tool_registry.py ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tool Registry System for CodeAct - Based on biomni's approach.
3
+ Provides centralized tool management with schema validation and discovery.
4
+ """
5
+
6
+ import ast
7
+ import importlib
8
+ import importlib.util
9
+ import inspect
10
+ import os
11
+ import pickle
12
+ from typing import Any, Dict, List, Optional, Callable
13
+ import pandas as pd
14
+
15
+ __all__ = ["ToolRegistry", "tool", "discover_tools_in_module", "create_module2api_from_functions"]
16
+
17
+
18
+ class ToolRegistry:
19
+ """
20
+ Central registry for managing tools, similar to biomni's ToolRegistry.
21
+ Handles tool registration, validation, and lookup by name/ID.
22
+ """
23
+
24
+ def __init__(self, module2api: Optional[Dict[str, List[Dict]]] = None):
25
+ """
26
+ Initialize the tool registry.
27
+
28
+ Args:
29
+ module2api: Dictionary mapping module names to lists of tool schemas
30
+ e.g., {"module.tools": [{"name": "func1", "description": "...", ...}]}
31
+ """
32
+ self.tools = []
33
+ self.next_id = 0
34
+ self._name_to_function = {} # Map tool names to actual functions
35
+
36
+ # Register tools from module2api if provided
37
+ if module2api:
38
+ for module_name, tool_list in module2api.items():
39
+ for tool_schema in tool_list:
40
+ self.register_tool(tool_schema, module_name)
41
+
42
+ # Create document dataframe for retrieval (similar to biomni)
43
+ self._create_document_df()
44
+
45
+ def register_tool(self, tool_schema: Dict, module_name: Optional[str] = None) -> bool:
46
+ """
47
+ Register a tool with the registry.
48
+
49
+ Args:
50
+ tool_schema: Tool schema dictionary with name, description, parameters
51
+ module_name: Optional module name where the tool function is located
52
+
53
+ Returns:
54
+ True if registration successful, False otherwise
55
+ """
56
+ if not self.validate_tool(tool_schema):
57
+ raise ValueError(f"Invalid tool format for {tool_schema.get('name', 'unknown')}")
58
+
59
+ # Add unique ID
60
+ tool_schema = tool_schema.copy()
61
+ tool_schema["id"] = self.next_id
62
+ tool_schema["module_name"] = module_name
63
+
64
+ # Try to load the actual function if module_name provided
65
+ if module_name and "name" in tool_schema:
66
+ try:
67
+ function = self._load_function_from_module(module_name, tool_schema["name"])
68
+ if function:
69
+ self._name_to_function[tool_schema["name"]] = function
70
+ except Exception as e:
71
+ print(f"Warning: Could not load function {tool_schema['name']} from {module_name}: {e}")
72
+
73
+ self.tools.append(tool_schema)
74
+ self.next_id += 1
75
+ return True
76
+
77
+ def validate_tool(self, tool_schema: Dict) -> bool:
78
+ """Validate that a tool schema has required fields."""
79
+ required_keys = ["name", "description"]
80
+ return all(key in tool_schema for key in required_keys)
81
+
82
+ def get_tool_by_name(self, name: str) -> Optional[Dict]:
83
+ """Get tool schema by name."""
84
+ for tool in self.tools:
85
+ if tool["name"] == name:
86
+ return tool
87
+ return None
88
+
89
+ def get_tool_by_id(self, tool_id: int) -> Optional[Dict]:
90
+ """Get tool schema by ID."""
91
+ for tool in self.tools:
92
+ if tool["id"] == tool_id:
93
+ return tool
94
+ return None
95
+
96
+ def get_function_by_name(self, name: str) -> Optional[Callable]:
97
+ """Get the actual function object by name."""
98
+ return self._name_to_function.get(name)
99
+
100
+ def get_all_functions(self) -> Dict[str, Callable]:
101
+ """Get all registered functions as a dictionary."""
102
+ return self._name_to_function.copy()
103
+
104
+ def list_tools(self) -> List[Dict]:
105
+ """List all registered tools with basic info."""
106
+ return [{"name": tool["name"], "id": tool["id"], "description": tool["description"]}
107
+ for tool in self.tools]
108
+
109
+ def list_tool_names(self) -> List[str]:
110
+ """Get list of all tool names."""
111
+ return [tool["name"] for tool in self.tools]
112
+
113
+ def add_function_directly(self, name: str, function: Callable, description: str = None) -> bool:
114
+ """
115
+ Add a function directly to the registry.
116
+
117
+ Args:
118
+ name: Function name
119
+ function: The callable function
120
+ description: Optional description, will be extracted from docstring if not provided
121
+
122
+ Returns:
123
+ True if added successfully
124
+ """
125
+ if description is None:
126
+ description = function.__doc__ or f"Function {name}"
127
+
128
+ # Create schema from function signature
129
+ schema = self._create_schema_from_function(name, function, description)
130
+
131
+ # Register the tool
132
+ self.register_tool(schema)
133
+ self._name_to_function[name] = function
134
+ return True
135
+
136
+ def _create_schema_from_function(self, name: str, function: Callable, description: str) -> Dict:
137
+ """Create a tool schema from a function object."""
138
+ sig = inspect.signature(function)
139
+
140
+ schema = {
141
+ "name": name,
142
+ "description": description,
143
+ "required_parameters": [],
144
+ "optional_parameters": []
145
+ }
146
+
147
+ for param_name, param in sig.parameters.items():
148
+ param_info = {
149
+ "name": param_name,
150
+ "type": self._get_param_type(param),
151
+ "description": f"Parameter {param_name}"
152
+ }
153
+
154
+ if param.default == inspect.Parameter.empty:
155
+ schema["required_parameters"].append(param_info)
156
+ else:
157
+ param_info["default"] = param.default
158
+ schema["optional_parameters"].append(param_info)
159
+
160
+ return schema
161
+
162
+ def _get_param_type(self, param: inspect.Parameter) -> str:
163
+ """Extract parameter type as string."""
164
+ if param.annotation != inspect.Parameter.empty:
165
+ if hasattr(param.annotation, '__name__'):
166
+ return param.annotation.__name__
167
+ else:
168
+ return str(param.annotation)
169
+ return "Any"
170
+
171
+ def _load_function_from_module(self, module_name: str, function_name: str) -> Optional[Callable]:
172
+ """Load a function from a module."""
173
+ try:
174
+ module = importlib.import_module(module_name)
175
+ return getattr(module, function_name, None)
176
+ except (ImportError, AttributeError):
177
+ return None
178
+
179
+ def _create_document_df(self):
180
+ """Create a pandas DataFrame for tool retrieval (similar to biomni)."""
181
+ docs = []
182
+ for tool in self.tools:
183
+ doc_content = {
184
+ "name": tool["name"],
185
+ "description": tool["description"],
186
+ "required_parameters": tool.get("required_parameters", []),
187
+ "optional_parameters": tool.get("optional_parameters", []),
188
+ "module_name": tool.get("module_name", "")
189
+ }
190
+ docs.append([tool["id"], doc_content])
191
+
192
+ self.document_df = pd.DataFrame(docs, columns=["docid", "document_content"])
193
+
194
+ def remove_tool_by_name(self, name: str) -> bool:
195
+ """Remove a tool by name."""
196
+ tool = self.get_tool_by_name(name)
197
+ if tool:
198
+ self.tools = [t for t in self.tools if t["name"] != name]
199
+ self._name_to_function.pop(name, None)
200
+ self._create_document_df() # Refresh document df
201
+ return True
202
+ return False
203
+
204
+ def save_registry(self, filename: str):
205
+ """Save registry to file."""
206
+ with open(filename, "wb") as file:
207
+ pickle.dump(self, file)
208
+
209
+ @staticmethod
210
+ def load_registry(filename: str) -> 'ToolRegistry':
211
+ """Load registry from file."""
212
+ with open(filename, "rb") as file:
213
+ return pickle.load(file)
214
+
215
+
216
+ # ====================
217
+ # TOOL DECORATOR
218
+ # ====================
219
+
220
+ def tool(func: Callable = None, *, name: str = None, description: str = None):
221
+ """
222
+ Decorator to mark functions as tools, similar to biomni's @tool decorator.
223
+
224
+ Usage:
225
+ @tool
226
+ def my_function(x: int) -> int:
227
+ '''This function does something'''
228
+ return x * 2
229
+
230
+ @tool(name="custom_name", description="Custom description")
231
+ def another_function():
232
+ pass
233
+ """
234
+ def decorator(f):
235
+ # Store metadata on the function
236
+ f._tool_name = name or f.__name__
237
+ f._tool_description = description or f.__doc__ or f"Function {f.__name__}"
238
+ f._is_tool = True
239
+ return f
240
+
241
+ if func is None:
242
+ # Called with arguments: @tool(name="...", description="...")
243
+ return decorator
244
+ else:
245
+ # Called without arguments: @tool
246
+ return decorator(func)
247
+
248
+
249
+ # ====================
250
+ # TOOL DISCOVERY UTILITIES
251
+ # ====================
252
+
253
+ def discover_tools_in_module(module_path: str) -> List[Callable]:
254
+ """
255
+ Discover all functions marked with @tool decorator in a module.
256
+
257
+ Args:
258
+ module_path: Path to the Python module file
259
+
260
+ Returns:
261
+ List of function objects marked as tools
262
+ """
263
+ with open(module_path, 'r') as file:
264
+ tree = ast.parse(file.read(), filename=module_path)
265
+
266
+ tool_function_names = []
267
+
268
+ # Find functions with @tool decorator
269
+ for node in ast.walk(tree):
270
+ if isinstance(node, ast.FunctionDef):
271
+ for decorator in node.decorator_list:
272
+ if (isinstance(decorator, ast.Name) and decorator.id == "tool") or \
273
+ (isinstance(decorator, ast.Call) and
274
+ isinstance(decorator.func, ast.Name) and decorator.func.id == "tool"):
275
+ tool_function_names.append(node.name)
276
+ break
277
+
278
+ # Import the module and get function objects
279
+ spec = importlib.util.spec_from_file_location("temp_module", module_path)
280
+ module = importlib.util.module_from_spec(spec)
281
+ spec.loader.exec_module(module)
282
+
283
+ tool_functions = []
284
+ for name in tool_function_names:
285
+ func = getattr(module, name, None)
286
+ if func and hasattr(func, '_is_tool'):
287
+ tool_functions.append(func)
288
+
289
+ return tool_functions
290
+
291
+
292
+ def create_module2api_from_functions(functions: List[Callable], module_name: str = "custom_tools") -> Dict[str, List[Dict]]:
293
+ """
294
+ Create a module2api dictionary from a list of functions.
295
+
296
+ Args:
297
+ functions: List of function objects
298
+ module_name: Name to assign to the module
299
+
300
+ Returns:
301
+ Dictionary in module2api format
302
+ """
303
+ tool_schemas = []
304
+
305
+ for func in functions:
306
+ name = getattr(func, '_tool_name', func.__name__)
307
+ description = getattr(func, '_tool_description', func.__doc__ or f"Function {func.__name__}")
308
+
309
+ # Create schema from function signature
310
+ registry = ToolRegistry()
311
+ schema = registry._create_schema_from_function(name, func, description)
312
+ tool_schemas.append(schema)
313
+
314
+ return {module_name: tool_schemas}
managers/tools/tool_selector.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tool Selector for CodeAct Agent.
3
+ Pure LLM-based tool selection mechanism similar to Biomni's prompt_based_retrieval.
4
+ """
5
+
6
+ import re
7
+ from typing import Dict, List, Optional
8
+ from langchain_core.messages import HumanMessage
9
+ from langchain_core.language_models.chat_models import BaseChatModel
10
+
11
+
12
+ class ToolSelector:
13
+ """
14
+ LLM-based tool selection system inspired by Biomni's approach.
15
+ Uses an LLM to intelligently select the most relevant tools for a given task.
16
+ """
17
+
18
+ def __init__(self, model: BaseChatModel):
19
+ """
20
+ Initialize the ToolSelector.
21
+
22
+ Args:
23
+ model: The language model to use for tool selection
24
+ """
25
+ self.model = model
26
+
27
+ def select_tools_for_task(self, query: str, available_tools: Dict[str, Dict], max_tools: int = 15) -> List[str]:
28
+ """
29
+ Use LLM-based selection to choose the most relevant tools for a query.
30
+ Inspired by Biomni's prompt_based_retrieval mechanism.
31
+
32
+ Args:
33
+ query: The user's query/task description
34
+ available_tools: Dictionary of {tool_name: tool_info} available
35
+ max_tools: Maximum number of tools to select
36
+
37
+ Returns:
38
+ List of selected tool names
39
+ """
40
+ if not available_tools:
41
+ return []
42
+
43
+ # Format tools for LLM prompt
44
+ tools_list = self._format_tools_for_prompt(available_tools)
45
+
46
+ # Create selection prompt (similar to Biomni's approach)
47
+ selection_prompt = f"""You are an expert biomedical research assistant. Your task is to select the most relevant tools to help answer a user's query.
48
+
49
+ USER QUERY: {query}
50
+
51
+ Below are the available tools. Select items that are directly or indirectly relevant to answering the query.
52
+ Be generous in your selection - include tools that might be useful for the task, even if they're not explicitly mentioned in the query.
53
+ It's better to include slightly more tools than to miss potentially useful ones.
54
+
55
+ AVAILABLE TOOLS:
56
+ {tools_list}
57
+
58
+ Select up to {max_tools} tools that would be most helpful for this task.
59
+
60
+ Respond with ONLY a comma-separated list of the exact tool names, like this:
61
+ tool_name_1, tool_name_2, tool_name_3
62
+
63
+ Selected tools:"""
64
+
65
+ try:
66
+ # Get LLM response
67
+ response = self.model.invoke([HumanMessage(content=selection_prompt)])
68
+ response_content = response.content.strip()
69
+
70
+ # Parse the response to extract tool names
71
+ selected_tools = self._parse_tool_selection_response(response_content, available_tools)
72
+
73
+ # Ensure we don't exceed max_tools
74
+ return selected_tools[:max_tools]
75
+
76
+ except Exception as e:
77
+ print(f"Error in LLM-based tool selection: {e}")
78
+ # Return all tools if LLM fails (no keyword fallback)
79
+ return list(available_tools.keys())[:max_tools]
80
+
81
+ def _format_tools_for_prompt(self, tools: Dict[str, Dict]) -> str:
82
+ """Format tools for the LLM prompt."""
83
+ formatted = []
84
+ for i, (tool_name, tool_info) in enumerate(tools.items(), 1):
85
+ description = tool_info.get('description', 'No description available')
86
+ source = tool_info.get('source', 'unknown')
87
+ formatted.append(f"{i}. {tool_name} ({source}): {description}")
88
+ return "\n".join(formatted)
89
+
90
+ def _parse_tool_selection_response(self, response: str, available_tools: Dict[str, Dict]) -> List[str]:
91
+ """Parse the LLM response to extract valid tool names."""
92
+ selected_tools = []
93
+
94
+ # Split by commas and clean up
95
+ tool_candidates = [name.strip() for name in response.split(',')]
96
+
97
+ for candidate in tool_candidates:
98
+ # Remove any extra characters, numbers, or formatting
99
+ clean_candidate = re.sub(r'^\d+\.\s*', '', candidate) # Remove "1. " prefixes
100
+ clean_candidate = clean_candidate.strip()
101
+
102
+ # Check if this matches any available tool (case-insensitive)
103
+ for tool_name in available_tools.keys():
104
+ if clean_candidate.lower() == tool_name.lower():
105
+ if tool_name not in selected_tools: # Avoid duplicates
106
+ selected_tools.append(tool_name)
107
+ break
108
+
109
+ return selected_tools
managers/workflow/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Workflow subsystem - Workflow execution, state, and plan management.
3
+ """
4
+
5
+ from .workflow_engine import WorkflowEngine
6
+ from .state_manager import StateManager
7
+ from .plan_manager import PlanManager
8
+
9
+ __all__ = ['WorkflowEngine', 'StateManager', 'PlanManager']
managers/workflow/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (523 Bytes). View file
 
managers/workflow/__pycache__/plan_manager.cpython-311.pyc ADDED
Binary file (3.91 kB). View file
 
managers/workflow/__pycache__/state_manager.cpython-311.pyc ADDED
Binary file (1.29 kB). View file
 
managers/workflow/__pycache__/workflow_engine.cpython-311.pyc ADDED
Binary file (16.3 kB). View file
 
managers/workflow/plan_manager.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Plan Manager for CodeAct Agent.
3
+ Handles plan creation, updates, and progress tracking.
4
+ """
5
+
6
+ import re
7
+ from typing import Optional, Dict
8
+
9
+
10
+ class PlanManager:
11
+ """Manages plan creation, updates, and progress tracking."""
12
+
13
+ @staticmethod
14
+ def extract_plan_from_content(content: str) -> Optional[str]:
15
+ """Extract plan from agent content."""
16
+ plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
17
+ matches = re.findall(plan_pattern, content)
18
+ # Return the last (most recent) plan if multiple plans exist
19
+ return matches[-1] if matches else None
20
+
21
+ @staticmethod
22
+ def update_plan_for_solution(plan_text: str) -> str:
23
+ """Update plan to mark all remaining steps as completed when providing final solution."""
24
+ if not plan_text:
25
+ return plan_text
26
+
27
+ lines = plan_text.split('\n')
28
+ updated_lines = []
29
+
30
+ for line in lines:
31
+ # Mark any unchecked or failed steps as completed since we're providing final solution
32
+ if '[ ]' in line or '[✗]' in line:
33
+ updated_line = re.sub(r'\[\s*[^\]]*\]', '[✓]', line)
34
+ updated_lines.append(updated_line)
35
+ else:
36
+ updated_lines.append(line)
37
+
38
+ return '\n'.join(updated_lines)
39
+
40
+ @staticmethod
41
+ def mark_step_completed(plan_text: str, step_description: str) -> str:
42
+ """Mark a specific step as completed based on its description."""
43
+ if not plan_text or not step_description:
44
+ return plan_text
45
+
46
+ lines = plan_text.split('\n')
47
+ updated_lines = []
48
+
49
+ for line in lines:
50
+ # Check if this line contains the step description and is unchecked
51
+ if step_description.lower() in line.lower() and ('[ ]' in line or '[✗]' in line):
52
+ updated_line = re.sub(r'\[\s*[^\]]*\]', '[✓]', line)
53
+ updated_lines.append(updated_line)
54
+ else:
55
+ updated_lines.append(line)
56
+
57
+ return '\n'.join(updated_lines)
58
+
59
+ @staticmethod
60
+ def get_plan_progress(plan_text: str) -> Dict[str, int]:
61
+ """Get plan progress statistics."""
62
+ if not plan_text:
63
+ return {"total": 0, "completed": 0, "pending": 0, "failed": 0}
64
+
65
+ lines = plan_text.split('\n')
66
+ stats = {"total": 0, "completed": 0, "pending": 0, "failed": 0}
67
+
68
+ for line in lines:
69
+ if re.search(r'\d+\.\s*\[', line):
70
+ stats["total"] += 1
71
+ if '[✓]' in line:
72
+ stats["completed"] += 1
73
+ elif '[ ]' in line:
74
+ stats["pending"] += 1
75
+ elif '[✗]' in line:
76
+ stats["failed"] += 1
77
+
78
+ return stats
managers/workflow/state_manager.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ State Manager for CodeAct Agent.
3
+ Manages agent state creation and manipulation.
4
+ """
5
+
6
+ import time
7
+ from typing import List, Dict, Optional
8
+
9
+
10
+ class StateManager:
11
+ """Manages agent state creation and manipulation."""
12
+
13
+ @staticmethod
14
+ def create_state_dict(messages: List = None, step_count: int = 0,
15
+ error_count: int = 0, start_time: float = None,
16
+ current_plan: str = None) -> Dict:
17
+ """Create a standardized state dictionary."""
18
+ return {
19
+ "messages": messages or [],
20
+ "step_count": step_count,
21
+ "error_count": error_count,
22
+ "start_time": start_time or time.time(),
23
+ "current_plan": current_plan
24
+ }
managers/workflow/workflow_engine.py ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Workflow Engine Manager for CodeAct Agent.
3
+ Manages the LangGraph workflow execution.
4
+ """
5
+
6
+ import re
7
+ import json
8
+ import datetime
9
+ from typing import Dict, Tuple, List, Any
10
+ from pathlib import Path
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn
12
+ from rich.rule import Rule
13
+ from langchain_core.messages import AIMessage, HumanMessage, BaseMessage
14
+ from langgraph.graph import StateGraph, START, END
15
+ from core.types import AgentState, AgentConfig
16
+ from .plan_manager import PlanManager
17
+
18
+
19
+ class WorkflowEngine:
20
+ """Manages the LangGraph workflow execution."""
21
+
22
+ def __init__(self, model, config: AgentConfig, console_display, state_manager):
23
+ self.model = model
24
+ self.config = config
25
+ self.console = console_display
26
+ self.state_manager = state_manager
27
+ self.plan_manager = PlanManager()
28
+ self.graph = None
29
+ self.trace_logs = [] # Store all trace logs
30
+ self.message_history = [] # Store all messages
31
+
32
+ def setup_workflow(self, generate_func, execute_func, should_continue_func):
33
+ """Setup the LangGraph workflow with provided functions."""
34
+ workflow = StateGraph(AgentState)
35
+
36
+ workflow.add_node("generate", generate_func)
37
+ workflow.add_node("execute", execute_func)
38
+
39
+ workflow.add_edge(START, "generate")
40
+ workflow.add_edge("execute", "generate")
41
+
42
+ workflow.add_conditional_edges("generate", should_continue_func, {
43
+ "end": END,
44
+ "execute": "execute"
45
+ })
46
+
47
+ self.graph = workflow.compile()
48
+
49
+ def run_workflow(self, initial_state: Dict) -> Tuple:
50
+ """Execute the workflow and handle display.
51
+
52
+ Returns:
53
+ tuple: (result_content, final_state)
54
+ """
55
+ # Clear previous traces for new run
56
+ self.trace_logs = []
57
+ self.message_history = []
58
+
59
+ # Track if final solution has been provided
60
+ final_solution_provided = False
61
+ previous_plan = None
62
+ displayed_reasoning = set()
63
+
64
+ # Stream the workflow execution with monitoring
65
+ self.console.console.print(Rule(title="Execution Steps", style="yellow"))
66
+
67
+ with Progress(
68
+ SpinnerColumn(),
69
+ TextColumn("[progress.description]{task.description}"),
70
+ console=self.console.console,
71
+ transient=True
72
+ ) as progress:
73
+ task = progress.add_task("Executing agent...", total=None)
74
+
75
+ final_state = None
76
+ for s in self.graph.stream(initial_state, stream_mode="values"):
77
+ step_count = s.get("step_count", 0)
78
+ current_plan = s.get("current_plan")
79
+ final_state = s
80
+
81
+ progress.update(task, description=f"Step {step_count}")
82
+
83
+ message = s["messages"][-1]
84
+
85
+ # Serialize and store the message
86
+ serialized_msg = self._serialize_message(message)
87
+ self.message_history.append(serialized_msg)
88
+
89
+ # Process different types of messages
90
+ if isinstance(message, AIMessage):
91
+ self._process_ai_message(
92
+ message, step_count, current_plan, previous_plan,
93
+ displayed_reasoning, final_solution_provided
94
+ )
95
+ if current_plan != previous_plan:
96
+ previous_plan = current_plan
97
+
98
+ elif "<observation>" in message.content:
99
+ self._process_observation_message(message, step_count)
100
+
101
+ result_content = final_state["messages"][-1].content if final_state else ""
102
+ return result_content, final_state
103
+
104
+ def _process_ai_message(self, message, step_count, current_plan, previous_plan,
105
+ displayed_reasoning, final_solution_provided):
106
+ """Process AI message and display appropriate panels."""
107
+ full_content = message.content
108
+
109
+ # 1. REASONING: Extract and display agent's thinking
110
+ thinking_content = self._extract_thinking_content(full_content)
111
+ if thinking_content and len(thinking_content) > 20:
112
+ content_hash = hash(thinking_content.strip())
113
+ if content_hash not in displayed_reasoning:
114
+ self.console.print_reasoning(thinking_content, step_count)
115
+ displayed_reasoning.add(content_hash)
116
+ # Add trace entry for reasoning
117
+ self._add_trace_entry("reasoning", step_count, thinking_content)
118
+
119
+ # 2. PLAN: Show plan only when it has changed
120
+ if (current_plan and current_plan != previous_plan and
121
+ self.config.verbose and not final_solution_provided):
122
+ self.console.print_plan(current_plan)
123
+ # Add trace entry for plan
124
+ self._add_trace_entry("plan", step_count, current_plan)
125
+
126
+ # 3. ACTION & CODE: Handle different action types
127
+ if "<execute>" in full_content and "</execute>" in full_content:
128
+ execute_match = re.search(r"<execute>(.*?)</execute>", full_content, re.DOTALL)
129
+ if execute_match:
130
+ code = execute_match.group(1).strip()
131
+ self.console.print_code_execution(code, step_count)
132
+ # Add trace entry for code execution
133
+ self._add_trace_entry("code_execution", step_count, code)
134
+
135
+ elif "<solution>" in full_content and "</solution>" in full_content:
136
+ solution_match = re.search(r"<solution>(.*?)</solution>", full_content, re.DOTALL)
137
+ if solution_match:
138
+ # Update plan to mark all remaining steps as completed
139
+ if current_plan:
140
+ updated_plan = self.plan_manager.update_plan_for_solution(current_plan)
141
+ if updated_plan != current_plan:
142
+ self.console.print_plan(updated_plan)
143
+
144
+ solution = solution_match.group(1).strip()
145
+ self.console.print_solution(solution, step_count)
146
+ final_solution_provided = True
147
+ # Add trace entry for solution
148
+ self._add_trace_entry("solution", step_count, solution)
149
+
150
+ elif "<error>" in full_content:
151
+ error_match = re.search(r"<error>(.*?)</error>", full_content, re.DOTALL)
152
+ if error_match:
153
+ error_content = error_match.group(1).strip()
154
+ self.console.print_error(error_content, step_count)
155
+ # Add trace entry for error
156
+ self._add_trace_entry("error", step_count, error_content)
157
+
158
+ def _process_observation_message(self, message, step_count):
159
+ """Process observation message and display results."""
160
+ obs_match = re.search(r"<observation>(.*?)</observation>", message.content, re.DOTALL)
161
+ if obs_match:
162
+ observation = obs_match.group(1).strip()
163
+ formatted_output = self._truncate_to_20_rows(observation)
164
+ self.console.print_execution_result(formatted_output, step_count)
165
+ # Add trace entry for observation
166
+ self._add_trace_entry("observation", step_count, observation)
167
+
168
+ def _extract_thinking_content(self, content: str) -> str:
169
+ """Extract thinking content from the message, removing tags and plan information."""
170
+ # Remove specific tags but keep observation content for separate handling
171
+ content = re.sub(r'</?(execute|solution|error)>', '', content)
172
+
173
+ # Remove plan content (numbered lists with checkboxes)
174
+ plan_pattern = r'\d+\.\s*\[[^\]]*\]\s*[^\n]+(?:\n\d+\.\s*\[[^\]]*\]\s*[^\n]+)*'
175
+ content = re.sub(plan_pattern, '', content).strip()
176
+
177
+ # Remove observation blocks entirely
178
+ content = re.sub(r'<observation>.*?</observation>', '', content, flags=re.DOTALL)
179
+
180
+ # Clean up extra whitespace and empty lines
181
+ lines = [line.strip() for line in content.split('\n') if line.strip()]
182
+ return '\n'.join(lines)
183
+
184
+ def _truncate_to_20_rows(self, text: str) -> str:
185
+ """Truncate any text output to show only the first 20 rows."""
186
+ lines = text.split('\n')
187
+
188
+ if len(lines) > 20:
189
+ truncated = '\n'.join(lines[:20])
190
+ total_lines = len(lines)
191
+ truncated += f"\n\n⚠️ Output truncated to 20 rows. Full output contains {total_lines} rows."
192
+ return truncated
193
+
194
+ return text
195
+
196
+ def _add_trace_entry(self, step_type: str, step_count: int, content: Any, metadata: Dict = None):
197
+ """Add an entry to the trace log."""
198
+ entry = {
199
+ "timestamp": datetime.datetime.now().isoformat(),
200
+ "step_count": step_count,
201
+ "step_type": step_type,
202
+ "content": content,
203
+ "metadata": metadata or {}
204
+ }
205
+ self.trace_logs.append(entry)
206
+
207
+ def _serialize_message(self, message: BaseMessage) -> Dict:
208
+ """Serialize a message for saving."""
209
+ if isinstance(message, HumanMessage):
210
+ msg_type = "human"
211
+ elif isinstance(message, AIMessage):
212
+ msg_type = "ai"
213
+ else:
214
+ msg_type = "system"
215
+
216
+ return {
217
+ "type": msg_type,
218
+ "content": message.content,
219
+ "timestamp": datetime.datetime.now().isoformat()
220
+ }
221
+
222
+ def save_trace_to_file(self, filepath: str = None) -> str:
223
+ """Save the complete trace log to a JSON file."""
224
+ if filepath is None:
225
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
226
+ filepath = f"agent_trace_{timestamp}.json"
227
+
228
+ trace_data = {
229
+ "execution_time": datetime.datetime.now().isoformat(),
230
+ "config": {
231
+ "max_steps": self.config.max_steps,
232
+ "timeout_seconds": self.config.timeout_seconds,
233
+ "verbose": self.config.verbose
234
+ },
235
+ "messages": self.message_history,
236
+ "trace_logs": self.trace_logs
237
+ }
238
+
239
+ with open(filepath, 'w', encoding='utf-8') as f:
240
+ json.dump(trace_data, f, indent=2, ensure_ascii=False)
241
+
242
+ return filepath
243
+
244
+ def generate_summary(self) -> Dict:
245
+ """Generate a summary of the agent execution."""
246
+ summary = {
247
+ "total_steps": len(self.trace_logs),
248
+ "message_count": len(self.message_history),
249
+ "execution_flow": [],
250
+ "code_executions": [],
251
+ "observations": [],
252
+ "errors": [],
253
+ "final_solution": None
254
+ }
255
+
256
+ for entry in self.trace_logs:
257
+ step_info = {
258
+ "step": entry["step_count"],
259
+ "type": entry["step_type"],
260
+ "timestamp": entry["timestamp"]
261
+ }
262
+
263
+ if entry["step_type"] == "reasoning":
264
+ summary["execution_flow"].append({
265
+ **step_info,
266
+ "reasoning": entry["content"][:200] + "..." if len(entry["content"]) > 200 else entry["content"]
267
+ })
268
+ elif entry["step_type"] == "code_execution":
269
+ summary["code_executions"].append({
270
+ **step_info,
271
+ "code": entry["content"]
272
+ })
273
+ elif entry["step_type"] == "observation":
274
+ summary["observations"].append({
275
+ **step_info,
276
+ "output": entry["content"][:500] + "..." if len(entry["content"]) > 500 else entry["content"]
277
+ })
278
+ elif entry["step_type"] == "error":
279
+ summary["errors"].append({
280
+ **step_info,
281
+ "error": entry["content"]
282
+ })
283
+ elif entry["step_type"] == "solution":
284
+ summary["final_solution"] = {
285
+ **step_info,
286
+ "solution": entry["content"]
287
+ }
288
+
289
+ return summary
290
+
291
+ def save_summary_to_file(self, filepath: str = None) -> str:
292
+ """Save the execution summary to a JSON file."""
293
+ if filepath is None:
294
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
295
+ filepath = f"agent_summary_{timestamp}.json"
296
+
297
+ summary = self.generate_summary()
298
+ summary["timestamp"] = datetime.datetime.now().isoformat()
299
+
300
+ with open(filepath, 'w', encoding='utf-8') as f:
301
+ json.dump(summary, f, indent=2, ensure_ascii=False)
302
+
303
+ return filepath