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.
- massgen/__init__.py +1 -1
- massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
- massgen/backend/capabilities.py +39 -0
- massgen/backend/chat_completions.py +111 -197
- massgen/backend/claude.py +210 -181
- massgen/backend/gemini.py +1015 -1559
- massgen/backend/grok.py +3 -2
- massgen/backend/response.py +160 -220
- massgen/chat_agent.py +340 -20
- massgen/cli.py +399 -25
- massgen/config_builder.py +20 -54
- massgen/config_validator.py +931 -0
- massgen/configs/README.md +95 -10
- massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
- massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
- massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
- massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
- massgen/configs/memory/single_agent_compression_test.yaml +64 -0
- massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
- massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
- massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
- massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
- massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
- massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
- massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
- massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
- massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
- massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
- massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
- massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
- massgen/formatter/_gemini_formatter.py +61 -15
- massgen/memory/README.md +277 -0
- massgen/memory/__init__.py +26 -0
- massgen/memory/_base.py +193 -0
- massgen/memory/_compression.py +237 -0
- massgen/memory/_context_monitor.py +211 -0
- massgen/memory/_conversation.py +255 -0
- massgen/memory/_fact_extraction_prompts.py +333 -0
- massgen/memory/_mem0_adapters.py +257 -0
- massgen/memory/_persistent.py +687 -0
- massgen/memory/docker-compose.qdrant.yml +36 -0
- massgen/memory/docs/DESIGN.md +388 -0
- massgen/memory/docs/QUICKSTART.md +409 -0
- massgen/memory/docs/SUMMARY.md +319 -0
- massgen/memory/docs/agent_use_memory.md +408 -0
- massgen/memory/docs/orchestrator_use_memory.md +586 -0
- massgen/memory/examples.py +237 -0
- massgen/orchestrator.py +207 -7
- massgen/tests/memory/test_agent_compression.py +174 -0
- massgen/tests/memory/test_context_window_management.py +286 -0
- massgen/tests/memory/test_force_compression.py +154 -0
- massgen/tests/memory/test_simple_compression.py +147 -0
- massgen/tests/test_ag2_lesson_planner.py +223 -0
- massgen/tests/test_agent_memory.py +534 -0
- massgen/tests/test_config_validator.py +1156 -0
- massgen/tests/test_conversation_memory.py +382 -0
- massgen/tests/test_langgraph_lesson_planner.py +223 -0
- massgen/tests/test_orchestrator_memory.py +620 -0
- massgen/tests/test_persistent_memory.py +435 -0
- massgen/token_manager/token_manager.py +6 -0
- massgen/tool/__init__.py +2 -9
- massgen/tool/_decorators.py +52 -0
- massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
- massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
- massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
- massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
- massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
- massgen/tool/_manager.py +102 -16
- massgen/tool/_registered_tool.py +3 -0
- massgen/tool/_result.py +3 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
- massgen/backend/gemini_mcp_manager.py +0 -545
- massgen/backend/gemini_trackers.py +0 -344
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
massgen/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
406
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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]],
|
massgen/backend/capabilities.py
CHANGED
|
@@ -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)",
|