massgen 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (84) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
  3. massgen/backend/capabilities.py +39 -0
  4. massgen/backend/chat_completions.py +111 -197
  5. massgen/backend/claude.py +210 -181
  6. massgen/backend/gemini.py +1015 -1559
  7. massgen/backend/grok.py +3 -2
  8. massgen/backend/response.py +160 -220
  9. massgen/chat_agent.py +340 -20
  10. massgen/cli.py +399 -25
  11. massgen/config_builder.py +20 -54
  12. massgen/config_validator.py +931 -0
  13. massgen/configs/README.md +95 -10
  14. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  15. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  16. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  17. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  18. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  19. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
  20. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
  21. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
  22. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
  23. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
  24. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
  25. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
  26. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
  27. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
  28. massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
  29. massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
  30. massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
  31. massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
  32. massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
  33. massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
  34. massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
  35. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
  36. massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
  37. massgen/formatter/_gemini_formatter.py +61 -15
  38. massgen/memory/README.md +277 -0
  39. massgen/memory/__init__.py +26 -0
  40. massgen/memory/_base.py +193 -0
  41. massgen/memory/_compression.py +237 -0
  42. massgen/memory/_context_monitor.py +211 -0
  43. massgen/memory/_conversation.py +255 -0
  44. massgen/memory/_fact_extraction_prompts.py +333 -0
  45. massgen/memory/_mem0_adapters.py +257 -0
  46. massgen/memory/_persistent.py +687 -0
  47. massgen/memory/docker-compose.qdrant.yml +36 -0
  48. massgen/memory/docs/DESIGN.md +388 -0
  49. massgen/memory/docs/QUICKSTART.md +409 -0
  50. massgen/memory/docs/SUMMARY.md +319 -0
  51. massgen/memory/docs/agent_use_memory.md +408 -0
  52. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  53. massgen/memory/examples.py +237 -0
  54. massgen/orchestrator.py +207 -7
  55. massgen/tests/memory/test_agent_compression.py +174 -0
  56. massgen/tests/memory/test_context_window_management.py +286 -0
  57. massgen/tests/memory/test_force_compression.py +154 -0
  58. massgen/tests/memory/test_simple_compression.py +147 -0
  59. massgen/tests/test_ag2_lesson_planner.py +223 -0
  60. massgen/tests/test_agent_memory.py +534 -0
  61. massgen/tests/test_config_validator.py +1156 -0
  62. massgen/tests/test_conversation_memory.py +382 -0
  63. massgen/tests/test_langgraph_lesson_planner.py +223 -0
  64. massgen/tests/test_orchestrator_memory.py +620 -0
  65. massgen/tests/test_persistent_memory.py +435 -0
  66. massgen/token_manager/token_manager.py +6 -0
  67. massgen/tool/__init__.py +2 -9
  68. massgen/tool/_decorators.py +52 -0
  69. massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
  70. massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
  71. massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
  72. massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
  73. massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
  74. massgen/tool/_manager.py +102 -16
  75. massgen/tool/_registered_tool.py +3 -0
  76. massgen/tool/_result.py +3 -0
  77. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
  78. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
  79. massgen/backend/gemini_mcp_manager.py +0 -545
  80. massgen/backend/gemini_trackers.py +0 -344
  81. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
  82. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
  83. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
  84. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
massgen/__init__.py CHANGED
@@ -68,7 +68,7 @@ from .chat_agent import (
68
68
  from .message_templates import MessageTemplates, get_templates
69
69
  from .orchestrator import Orchestrator, create_orchestrator
70
70
 
71
- __version__ = "0.1.4"
71
+ __version__ = "0.1.6"
72
72
  __author__ = "MassGen Contributors"
73
73
 
74
74
 
@@ -11,16 +11,49 @@ import base64
11
11
  import json
12
12
  import mimetypes
13
13
  from abc import abstractmethod
14
+ from dataclasses import dataclass
14
15
  from pathlib import Path
15
- from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple
16
+ from typing import (
17
+ Any,
18
+ AsyncGenerator,
19
+ Callable,
20
+ Dict,
21
+ List,
22
+ NamedTuple,
23
+ Optional,
24
+ Set,
25
+ Tuple,
26
+ )
16
27
 
17
28
  import httpx
29
+ from pydantic import BaseModel
18
30
 
19
31
  from ..logger_config import log_backend_activity, logger
20
32
  from ..tool import ToolManager
33
+ from ..utils import CoordinationStage
21
34
  from .base import LLMBackend, StreamChunk
22
35
 
23
36
 
37
+ @dataclass
38
+ class ToolExecutionConfig:
39
+ """Configuration for unified tool execution.
40
+
41
+ Encapsulates all differences between custom and MCP tool execution,
42
+ enabling unified processing.
43
+ """
44
+
45
+ tool_type: str # "custom" or "mcp" for identification
46
+ chunk_type: str # "custom_tool_status" or "mcp_status" for StreamChunk type
47
+ emoji_prefix: str # "🔧 [Custom Tool]" or "🔧 [MCP Tool]" for display
48
+ success_emoji: str # "✅ [Custom Tool]" or "✅ [MCP Tool]" for completion
49
+ error_emoji: str # "❌ [Custom Tool Error]" or "❌ [MCP Tool Error]" for errors
50
+ source_prefix: str # "custom_" or "mcp_" for chunk source field
51
+ status_called: str # "custom_tool_called" or "mcp_tool_called"
52
+ status_response: str # "custom_tool_response" or "mcp_tool_response"
53
+ status_error: str # "custom_tool_error" or "mcp_tool_error"
54
+ execution_callback: Callable # reference to _execute_custom_tool or _execute_mcp_function_with_retry
55
+
56
+
24
57
  class UploadFileError(Exception):
25
58
  """Raised when an upload specified in configuration fails to process."""
26
59
 
@@ -29,6 +62,93 @@ class UnsupportedUploadSourceError(UploadFileError):
29
62
  """Raised when a provided upload source cannot be processed (e.g., URL without fetch support)."""
30
63
 
31
64
 
65
+ class CustomToolChunk(NamedTuple):
66
+ """Streaming chunk from custom tool execution."""
67
+
68
+ data: str # Chunk data to stream to user
69
+ completed: bool # True for the last chunk only
70
+ accumulated_result: str # Final accumulated result (only when completed=True)
71
+
72
+
73
+ class ExecutionContext(BaseModel):
74
+ """Execution context for MCP tool execution."""
75
+
76
+ messages: List[Dict[str, Any]] = []
77
+ agent_system_message: Optional[str] = None
78
+ agent_id: Optional[str] = None
79
+ backend_name: Optional[str] = None
80
+ current_stage: Optional[CoordinationStage] = None
81
+
82
+ # These will be computed after initialization
83
+ system_messages: Optional[List[Dict[str, Any]]] = None
84
+ user_messages: Optional[List[Dict[str, Any]]] = None
85
+ prompt: Optional[List[Dict[str, Any]]] = None
86
+
87
+ def __init__(
88
+ self,
89
+ messages: Optional[List[Dict[str, Any]]] = None,
90
+ agent_system_message: Optional[str] = None,
91
+ agent_id: Optional[str] = None,
92
+ backend_name: Optional[str] = None,
93
+ current_stage: Optional[CoordinationStage] = None,
94
+ ):
95
+ """Initialize execution context."""
96
+ super().__init__(
97
+ messages=messages or [],
98
+ agent_system_message=agent_system_message,
99
+ agent_id=agent_id,
100
+ backend_name=backend_name,
101
+ current_stage=current_stage,
102
+ )
103
+ # Now you can process messages after Pydantic initialization
104
+ self._process_messages()
105
+
106
+ def _process_messages(self) -> None:
107
+ """Process messages to extract commonly used fields."""
108
+ if self.messages:
109
+ self.system_messages = []
110
+ self.user_messages = []
111
+
112
+ for msg in self.messages:
113
+ role = msg.get("role")
114
+
115
+ if role == "system":
116
+ self.system_messages.append(msg)
117
+
118
+ if role == "user":
119
+ self.user_messages.append(msg)
120
+
121
+ if self.current_stage == CoordinationStage.INITIAL_ANSWER:
122
+ self.prompt = [self.exec_instruction()] + self.user_messages
123
+ elif self.current_stage == CoordinationStage.ENFORCEMENT:
124
+ self.prompt = self.user_messages
125
+ elif self.current_stage == CoordinationStage.PRESENTATION:
126
+ if len(self.system_messages) > 1:
127
+ raise ValueError("Execution Context expects only one system message during PRESENTATION stage")
128
+ system_message = self._filter_system_message(self.system_messages[0])
129
+ self.prompt = [system_message] + self.user_messages
130
+
131
+ # Todo: Temporary solution. We should change orchestrator not to preprend agent system message
132
+ def _filter_system_message(self, sys_msg):
133
+ """Filter out agent system message prefix from system message content."""
134
+ content = sys_msg.get("content", "")
135
+ # Remove agent_system_message prefix if present
136
+ if self.agent_system_message and isinstance(content, str):
137
+ if content.startswith(self.agent_system_message):
138
+ # Remove the prefix and any following whitespace/newlines
139
+ remaining = content[len(self.agent_system_message) :].lstrip("\n ")
140
+ sys_msg["content"] = remaining
141
+
142
+ return sys_msg
143
+
144
+ @staticmethod
145
+ def exec_instruction() -> dict:
146
+ instruction = (
147
+ "You MUST digest existing answers, combine their strengths, " "and do additional work to address their weaknesses, " "then generate a better answer to address the ORIGINAL MESSAGE."
148
+ )
149
+ return {"role": "system", "content": instruction}
150
+
151
+
32
152
  # MCP integration imports
33
153
  try:
34
154
  from ..mcp_tools import (
@@ -119,6 +239,9 @@ class CustomToolAndMCPBackend(LLMBackend):
119
239
  self.custom_tool_manager = ToolManager()
120
240
  self._custom_tool_names: set[str] = set()
121
241
 
242
+ # Store execution context for custom tool execution
243
+ self._execution_context = None
244
+
122
245
  # Register custom tools if provided
123
246
  custom_tools = kwargs.get("custom_tools", [])
124
247
  if custom_tools:
@@ -178,6 +301,7 @@ class CustomToolAndMCPBackend(LLMBackend):
178
301
  @abstractmethod
179
302
  async def _process_stream(self, stream, all_params, agent_id: Optional[str] = None) -> AsyncGenerator[StreamChunk, None]:
180
303
  """Process stream."""
304
+ yield StreamChunk(type="error", error="Not implemented")
181
305
 
182
306
  # Custom tools support
183
307
  def _register_custom_tools(self, custom_tools: List[Dict[str, Any]]) -> None:
@@ -402,14 +526,55 @@ class CustomToolAndMCPBackend(LLMBackend):
402
526
  )
403
527
  return None
404
528
 
405
- async def _execute_custom_tool(self, call: Dict[str, Any]) -> str:
406
- """Execute a custom tool and return the result.
529
+ async def _stream_execution_results(
530
+ self,
531
+ tool_request: Dict[str, Any],
532
+ ) -> AsyncGenerator[Tuple[str, bool], None]:
533
+ """Stream execution results from tool manager, yielding (data, is_log) tuples.
534
+
535
+ Args:
536
+ tool_request: Tool request dictionary with name and input
537
+
538
+ Yields:
539
+ Tuple of (data: str, is_log: bool) for each result block
540
+ """
541
+ try:
542
+ async for result in self.custom_tool_manager.execute_tool(
543
+ tool_request,
544
+ execution_context=self._execution_context.model_dump(),
545
+ ):
546
+ is_log = getattr(result, "is_log", False)
547
+
548
+ if hasattr(result, "output_blocks"):
549
+ for block in result.output_blocks:
550
+ data = ""
551
+ if hasattr(block, "data"):
552
+ data = str(block.data)
553
+
554
+ if data:
555
+ yield (data, is_log)
556
+
557
+ except Exception as e:
558
+ logger.error(f"Error in custom tool execution: {e}")
559
+ yield (f"Error: {str(e)}", True)
560
+
561
+ async def stream_custom_tool_execution(
562
+ self,
563
+ call: Dict[str, Any],
564
+ ) -> AsyncGenerator[CustomToolChunk, None]:
565
+ """Stream custom tool execution with differentiation between logs and final results.
566
+
567
+ This method:
568
+ - Streams all results (logs and final) to users in real-time
569
+ - Accumulates only is_log=False results for message history
570
+ - Yields CustomToolChunk with completed=False for intermediate results
571
+ - Yields final CustomToolChunk with completed=True and accumulated result
407
572
 
408
573
  Args:
409
574
  call: Function call dictionary with name and arguments
410
575
 
411
- Returns:
412
- The execution result as a string
576
+ Yields:
577
+ CustomToolChunk instances for streaming to user
413
578
  """
414
579
  import json
415
580
 
@@ -428,30 +593,270 @@ class CustomToolAndMCPBackend(LLMBackend):
428
593
  "input": arguments,
429
594
  }
430
595
 
431
- result_text = ""
432
- try:
433
- async for result in self.custom_tool_manager.execute_tool(tool_request):
434
- # Accumulate results
435
- if hasattr(result, "output_blocks"):
436
- for block in result.output_blocks:
437
- if hasattr(block, "data"):
438
- result_text += str(block.data)
439
- elif hasattr(block, "content"):
440
- result_text += str(block.content)
441
- elif hasattr(result, "content"):
442
- result_text += str(result.content)
443
- else:
444
- result_text += str(result)
445
- except Exception as e:
446
- logger.error(f"Error in custom tool execution: {e}")
447
- result_text = f"Error: {str(e)}"
596
+ accumulated_result = ""
597
+
598
+ # Stream all results and accumulate only is_log=True
599
+ async for data, is_log in self._stream_execution_results(tool_request):
600
+ # Yield streaming chunk to user
601
+ yield CustomToolChunk(
602
+ data=data,
603
+ completed=False,
604
+ accumulated_result="",
605
+ )
448
606
 
449
- return result_text or "Tool executed successfully"
607
+ # Accumulate only final results for message history
608
+ if not is_log:
609
+ accumulated_result += data
610
+
611
+ # Yield final chunk with accumulated result
612
+ yield CustomToolChunk(
613
+ data="",
614
+ completed=True,
615
+ accumulated_result=accumulated_result or "Tool executed successfully",
616
+ )
450
617
 
451
618
  def _get_custom_tools_schemas(self) -> List[Dict[str, Any]]:
452
619
  """Get OpenAI-formatted schemas for all registered custom tools."""
453
620
  return self.custom_tool_manager.fetch_tool_schemas()
454
621
 
622
+ def _categorize_tool_calls(
623
+ self,
624
+ captured_calls: List[Dict[str, Any]],
625
+ ) -> Tuple[List[Dict], List[Dict], List[Dict]]:
626
+ """Categorize tool calls into MCP, custom, and provider categories.
627
+
628
+ Args:
629
+ captured_calls: List of tool call dictionaries with name and arguments
630
+
631
+ Returns:
632
+ Tuple of (mcp_calls, custom_calls, provider_calls)
633
+
634
+ Note:
635
+ Provider calls include workflow tools (new_answer, vote) that must NOT
636
+ be executed by backends.
637
+ """
638
+ mcp_calls: List[Dict] = []
639
+ custom_calls: List[Dict] = []
640
+ provider_calls: List[Dict] = []
641
+
642
+ for call in captured_calls:
643
+ call_name = call.get("name", "")
644
+
645
+ if call_name in self._mcp_functions:
646
+ mcp_calls.append(call)
647
+ elif call_name in self._custom_tool_names:
648
+ custom_calls.append(call)
649
+ else:
650
+ # Provider calls include workflow tools and unknown tools
651
+ provider_calls.append(call)
652
+
653
+ return mcp_calls, custom_calls, provider_calls
654
+
655
+ @abstractmethod
656
+ def _append_tool_result_message(
657
+ self,
658
+ updated_messages: List[Dict[str, Any]],
659
+ call: Dict[str, Any],
660
+ result: Any,
661
+ tool_type: str,
662
+ ) -> None:
663
+ """Append tool result to messages in backend-specific format.
664
+
665
+ Args:
666
+ updated_messages: Message list to append to
667
+ call: Tool call dictionary with call_id, name, arguments
668
+ result: Tool execution result
669
+ tool_type: "custom" or "mcp"
670
+
671
+ Each backend must implement this to use its specific message format:
672
+ - ChatCompletions: {"role": "tool", "tool_call_id": ..., "content": ...}
673
+ - Response API: {"type": "function_call_output", "call_id": ..., "output": ...}
674
+ - Claude: {"role": "user", "content": [{"type": "tool_result", "tool_use_id": ..., "content": ...}]}
675
+ """
676
+
677
+ @abstractmethod
678
+ def _append_tool_error_message(
679
+ self,
680
+ updated_messages: List[Dict[str, Any]],
681
+ call: Dict[str, Any],
682
+ error_msg: str,
683
+ tool_type: str,
684
+ ) -> None:
685
+ """Append tool error to messages in backend-specific format.
686
+
687
+ Args:
688
+ updated_messages: Message list to append to
689
+ call: Tool call dictionary with call_id, name, arguments
690
+ error_msg: Error message string
691
+ tool_type: "custom" or "mcp"
692
+
693
+ Each backend must implement this using the same format as _append_tool_result_message
694
+ but with error content.
695
+ """
696
+
697
+ async def _execute_tool_with_logging(
698
+ self,
699
+ call: Dict[str, Any],
700
+ config: ToolExecutionConfig,
701
+ updated_messages: List[Dict[str, Any]],
702
+ processed_call_ids: Set[str],
703
+ ) -> AsyncGenerator[StreamChunk, None]:
704
+ """Execute a tool with unified logging and error handling.
705
+
706
+ Args:
707
+ call: Tool call dictionary with call_id, name, arguments
708
+ config: ToolExecutionConfig specifying execution parameters
709
+ updated_messages: Message list to append results to
710
+ processed_call_ids: Set to track processed call IDs
711
+
712
+ Yields:
713
+ StreamChunk objects for status updates, arguments, results, and errors
714
+
715
+ Note:
716
+ This method provides defense-in-depth validation to prevent workflow tools
717
+ from being executed. Workflow tools should be filtered by categorization
718
+ logic before reaching this method.
719
+ """
720
+ tool_name = call.get("name", "")
721
+
722
+ if tool_name in ["new_answer", "vote"]:
723
+ error_msg = f"CRITICAL: Workflow tool {tool_name} incorrectly routed to execution"
724
+ logger.error(error_msg)
725
+ yield StreamChunk(
726
+ type=config.chunk_type,
727
+ status=config.status_error,
728
+ content=f"{config.error_emoji} {error_msg}",
729
+ source=f"{config.source_prefix}{tool_name}",
730
+ )
731
+ return
732
+
733
+ try:
734
+ # Yield tool called status
735
+ yield StreamChunk(
736
+ type=config.chunk_type,
737
+ status=config.status_called,
738
+ content=f"{config.emoji_prefix} Calling {tool_name}...",
739
+ source=f"{config.source_prefix}{tool_name}",
740
+ )
741
+
742
+ # Yield arguments chunk
743
+ arguments_str = call.get("arguments", "{}")
744
+ yield StreamChunk(
745
+ type=config.chunk_type,
746
+ status="function_call",
747
+ content=f"Arguments for Calling {tool_name}: {arguments_str}",
748
+ source=f"{config.source_prefix}{tool_name}",
749
+ )
750
+
751
+ # Execute tool via callback
752
+ result = None
753
+ result_str = ""
754
+ result_obj = None
755
+
756
+ if config.tool_type == "custom":
757
+ # Check if execution_callback returns an async generator (streaming)
758
+ callback_result = config.execution_callback(call)
759
+
760
+ # Handle async generator (streaming custom tools)
761
+ if hasattr(callback_result, "__aiter__"):
762
+ # This is an async generator - stream intermediate results
763
+ async for chunk in callback_result:
764
+ # Yield intermediate chunks if available
765
+ if hasattr(chunk, "data") and chunk.data and not chunk.completed:
766
+ # Stream intermediate output to user
767
+ yield StreamChunk(
768
+ type=config.chunk_type,
769
+ status="custom_tool_output",
770
+ content=chunk.data,
771
+ source=f"{config.source_prefix}{tool_name}",
772
+ )
773
+ elif hasattr(chunk, "completed") and chunk.completed:
774
+ # Extract final accumulated result
775
+ result_str = chunk.accumulated_result
776
+ result = result_str
777
+ else:
778
+ # Handle regular await (non-streaming custom tools)
779
+ result = await callback_result
780
+ result_str = str(result)
781
+ else: # MCP
782
+ result_str, result_obj = await config.execution_callback(call["name"], call["arguments"])
783
+ result = result_str
784
+
785
+ # Check for MCP failure after retries
786
+ if config.tool_type == "mcp" and result_str.startswith("Error:"):
787
+ logger.warning(f"MCP tool {tool_name} failed after retries: {result_str}")
788
+ error_msg = result_str
789
+ self._append_tool_error_message(updated_messages, call, error_msg, config.tool_type)
790
+ processed_call_ids.add(call.get("call_id", ""))
791
+ yield StreamChunk(
792
+ type=config.chunk_type,
793
+ status=config.status_error,
794
+ content=f"{config.error_emoji} {error_msg}",
795
+ source=f"{config.source_prefix}{tool_name}",
796
+ )
797
+ return
798
+
799
+ # Append result to messages
800
+ self._append_tool_result_message(updated_messages, call, result, config.tool_type)
801
+
802
+ # Yield results chunk
803
+ # For MCP tools, try to extract text from result_obj if available
804
+ display_result = result_str
805
+ if config.tool_type == "mcp" and result_obj:
806
+ try:
807
+ if hasattr(result_obj, "content") and isinstance(result_obj.content, list):
808
+ if len(result_obj.content) > 0 and hasattr(result_obj.content[0], "text"):
809
+ display_result = result_obj.content[0].text
810
+ except (AttributeError, IndexError, TypeError):
811
+ pass # Fall back to result_str
812
+
813
+ yield StreamChunk(
814
+ type=config.chunk_type,
815
+ status="function_call_output",
816
+ content=f"Results for Calling {tool_name}: {display_result}",
817
+ source=f"{config.source_prefix}{tool_name}",
818
+ )
819
+
820
+ # Yield completion status
821
+ yield StreamChunk(
822
+ type=config.chunk_type,
823
+ status=config.status_response,
824
+ content=f"{config.success_emoji} {tool_name} completed",
825
+ source=f"{config.source_prefix}{tool_name}",
826
+ )
827
+
828
+ processed_call_ids.add(call.get("call_id", ""))
829
+ logger.info(f"Executed {config.tool_type} tool: {tool_name}")
830
+
831
+ except Exception as e:
832
+ # Log error
833
+ logger.error(f"Error executing {config.tool_type} tool {tool_name}: {e}")
834
+
835
+ # Build error message
836
+ error_msg = f"Error executing {tool_name}: {str(e)}"
837
+
838
+ # Yield arguments chunk for context
839
+ arguments_str = call.get("arguments", "{}")
840
+ yield StreamChunk(
841
+ type=config.chunk_type,
842
+ status="function_call",
843
+ content=f"Arguments for Calling {tool_name}: {arguments_str}",
844
+ source=f"{config.source_prefix}{tool_name}",
845
+ )
846
+
847
+ # Yield error status chunk
848
+ yield StreamChunk(
849
+ type=config.chunk_type,
850
+ status=config.status_error,
851
+ content=f"{config.error_emoji} {error_msg}",
852
+ source=f"{config.source_prefix}{tool_name}",
853
+ )
854
+
855
+ # Append error to messages
856
+ self._append_tool_error_message(updated_messages, call, error_msg, config.tool_type)
857
+
858
+ processed_call_ids.add(call.get("call_id", ""))
859
+
455
860
  # MCP support methods
456
861
  async def _setup_mcp_tools(self) -> None:
457
862
  """Initialize MCP client for mcp_tools-based servers (stdio + streamable-http)."""
@@ -1046,6 +1451,15 @@ class CustomToolAndMCPBackend(LLMBackend):
1046
1451
 
1047
1452
  agent_id = kwargs.get("agent_id", None)
1048
1453
 
1454
+ # Build execution context for tools (generic, not tool-specific)
1455
+ self._execution_context = ExecutionContext(
1456
+ messages=messages,
1457
+ agent_system_message=kwargs.get("system_message", None),
1458
+ agent_id=self.agent_id,
1459
+ backend_name=self.backend_name,
1460
+ current_stage=self.coordination_stage,
1461
+ )
1462
+
1049
1463
  log_backend_activity(
1050
1464
  self.get_provider_name(),
1051
1465
  "Starting stream_with_tools",
@@ -1105,6 +1519,8 @@ class CustomToolAndMCPBackend(LLMBackend):
1105
1519
  except Exception as e:
1106
1520
  # Handle exceptions that occur during MCP setup (__aenter__) or teardown
1107
1521
  # Provide a clear user-facing message and fall back to non-MCP streaming
1522
+ client = None
1523
+
1108
1524
  try:
1109
1525
  client = self._create_client(**kwargs)
1110
1526
 
@@ -1131,6 +1547,20 @@ class CustomToolAndMCPBackend(LLMBackend):
1131
1547
  finally:
1132
1548
  await self._cleanup_client(client)
1133
1549
 
1550
+ @abstractmethod
1551
+ async def _stream_with_custom_and_mcp_tools(
1552
+ self,
1553
+ current_messages: List[Dict[str, Any]],
1554
+ tools: List[Dict[str, Any]],
1555
+ client,
1556
+ **kwargs,
1557
+ ) -> AsyncGenerator[StreamChunk, None]:
1558
+ yield StreamChunk(type="error", error="Not implemented")
1559
+
1560
+ @abstractmethod
1561
+ def _create_client(self, **kwargs):
1562
+ pass
1563
+
1134
1564
  async def _stream_without_custom_and_mcp_tools(
1135
1565
  self,
1136
1566
  messages: List[Dict[str, Any]],
@@ -272,6 +272,45 @@ BACKEND_CAPABILITIES: Dict[str, BackendCapabilities] = {
272
272
  env_var=None,
273
273
  notes="Local model hosting. Capabilities depend on loaded model.",
274
274
  ),
275
+ "zai": BackendCapabilities(
276
+ backend_type="zai",
277
+ provider_name="ZAI (Z.AI)",
278
+ supported_capabilities={
279
+ "mcp",
280
+ },
281
+ builtin_tools=[],
282
+ filesystem_support="mcp",
283
+ models=["glm-4.5", "custom"],
284
+ default_model="glm-4.5",
285
+ env_var="ZAI_API_KEY",
286
+ notes="OpenAI-compatible API from Z.AI. Supports GLM models.",
287
+ ),
288
+ "vllm": BackendCapabilities(
289
+ backend_type="vllm",
290
+ provider_name="vLLM",
291
+ supported_capabilities={
292
+ "mcp",
293
+ },
294
+ builtin_tools=[],
295
+ filesystem_support="mcp",
296
+ models=["custom"],
297
+ default_model="custom",
298
+ env_var=None,
299
+ notes="vLLM inference server. Local model hosting with high throughput.",
300
+ ),
301
+ "sglang": BackendCapabilities(
302
+ backend_type="sglang",
303
+ provider_name="SGLang",
304
+ supported_capabilities={
305
+ "mcp",
306
+ },
307
+ builtin_tools=[],
308
+ filesystem_support="mcp",
309
+ models=["custom"],
310
+ default_model="custom",
311
+ env_var=None,
312
+ notes="SGLang inference server. Fast local model serving.",
313
+ ),
275
314
  "inference": BackendCapabilities(
276
315
  backend_type="inference",
277
316
  provider_name="Inference (vLLM/SGLang)",