hud-python 0.3.4__py3-none-any.whl → 0.4.0__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 hud-python might be problematic. Click here for more details.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,250 +1,250 @@
1
- """LangChain MCP Agent implementation."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- from typing import TYPE_CHECKING, Any
7
-
8
- import mcp.types as types
9
- from langchain.agents import AgentExecutor, create_tool_calling_agent
10
- from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
11
- from langchain.schema import AIMessage, BaseMessage, HumanMessage, SystemMessage
12
- from mcp.types import CallToolRequestParams as MCPToolCall
13
- from mcp.types import CallToolResult as MCPToolResult
14
- from mcp_use.adapters.langchain_adapter import LangChainAdapter
15
-
16
- if TYPE_CHECKING:
17
- from langchain.schema.language_model import BaseLanguageModel
18
- from langchain_core.tools import BaseTool
19
- from .base import BaseMCPAgent, ModelResponse
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class LangChainMCPAgent(BaseMCPAgent):
25
- """
26
- LangChain agent that uses MCP servers for tool execution.
27
-
28
- This agent wraps any LangChain-compatible LLM and provides
29
- access to MCP tools through LangChain's tool-calling interface.
30
- """
31
-
32
- def __init__(
33
- self,
34
- llm: BaseLanguageModel,
35
- **kwargs: Any,
36
- ) -> None:
37
- """
38
- Initialize LangChain MCP agent.
39
-
40
- Args:
41
- llm: Any LangChain-compatible language model
42
- **kwargs: Additional arguments passed to BaseMCPAgent
43
- """
44
- super().__init__(**kwargs)
45
-
46
- self.llm = llm
47
- self.adapter = LangChainAdapter(disallowed_tools=self.disallowed_tools)
48
- self._langchain_tools: list[BaseTool] | None = None
49
-
50
- self.model_name = (
51
- "langchain-" + self.llm.model_name # type: ignore
52
- if hasattr(self.llm, "model_name")
53
- else "unknown"
54
- )
55
-
56
- def _get_langchain_tools(self) -> list[BaseTool]:
57
- """Get or create LangChain tools from MCP tools."""
58
- if self._langchain_tools is not None:
59
- return self._langchain_tools
60
-
61
- # Create LangChain tools from MCP tools using the adapter
62
- self._langchain_tools = []
63
-
64
- # Get tools grouped by connector
65
- tools_by_connector = self.get_tools_by_connector()
66
-
67
- # Convert tools using the adapter
68
- for connector, tools in tools_by_connector.items():
69
- langchain_tools = self.adapter._convert_tools(tools, connector) # type: ignore[reportAttributeAccessIssue]
70
- self._langchain_tools.extend(langchain_tools)
71
-
72
- logger.info("Created %s LangChain tools from MCP tools", len(self._langchain_tools))
73
- return self._langchain_tools
74
-
75
- async def create_initial_messages(
76
- self, prompt: str, screenshot: str | None
77
- ) -> list[BaseMessage]:
78
- """Create initial messages for LangChain."""
79
- messages = []
80
-
81
- # Add system message
82
- system_prompt = self.get_system_prompt()
83
- messages.append(SystemMessage(content=system_prompt))
84
-
85
- # Add user message with prompt and optional screenshot
86
- if screenshot:
87
- # For multimodal models, include the image
88
- content = [
89
- {"type": "text", "text": prompt},
90
- {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{screenshot}"}},
91
- ]
92
- messages.append(HumanMessage(content=content))
93
- else:
94
- messages.append(HumanMessage(content=prompt))
95
-
96
- return messages
97
-
98
- async def get_model_response(self, messages: list[BaseMessage]) -> ModelResponse:
99
- """Get response from LangChain model including any tool calls."""
100
- # Get LangChain tools (created lazily)
101
- langchain_tools = self._get_langchain_tools()
102
-
103
- # Create a prompt template from current messages
104
- # Extract system message if present
105
- system_content = "You are a helpful assistant"
106
- non_system_messages = []
107
-
108
- for msg in messages:
109
- if isinstance(msg, SystemMessage):
110
- system_content = str(msg.content)
111
- else:
112
- non_system_messages.append(msg)
113
-
114
- # Create prompt with placeholders
115
- prompt = ChatPromptTemplate.from_messages(
116
- [
117
- ("system", system_content),
118
- MessagesPlaceholder(variable_name="chat_history"),
119
- MessagesPlaceholder(variable_name="agent_scratchpad"),
120
- ]
121
- )
122
-
123
- # Create agent with tools
124
- agent = create_tool_calling_agent(
125
- llm=self.llm,
126
- tools=langchain_tools,
127
- prompt=prompt,
128
- )
129
-
130
- # Create executor
131
- executor = AgentExecutor(
132
- agent=agent,
133
- tools=langchain_tools,
134
- verbose=False,
135
- )
136
-
137
- # Format the last user message as input
138
- last_user_msg = None
139
- for msg in reversed(non_system_messages):
140
- if isinstance(msg, HumanMessage):
141
- last_user_msg = msg
142
- break
143
-
144
- if not last_user_msg:
145
- return ModelResponse(content="No user message found", tool_calls=[], done=True)
146
-
147
- # Extract text from message content
148
- input_text = ""
149
- if isinstance(last_user_msg.content, str):
150
- input_text = last_user_msg.content
151
- elif isinstance(last_user_msg.content, list):
152
- # Extract text from multimodal content
153
- for item in last_user_msg.content:
154
- if isinstance(item, dict) and item.get("type") == "text":
155
- input_text = item.get("text", "")
156
- break
157
-
158
- # Build chat history (exclude last user message and system)
159
- chat_history = []
160
- for _, msg in enumerate(non_system_messages[:-1]):
161
- if isinstance(msg, HumanMessage | AIMessage):
162
- chat_history.append(msg)
163
-
164
- # Execute the agent
165
- try:
166
- result = await executor.ainvoke(
167
- {
168
- "input": input_text,
169
- "chat_history": chat_history,
170
- }
171
- )
172
-
173
- # Process the result
174
- output = result.get("output", "")
175
-
176
- # Check if tools were called
177
- if result.get("intermediate_steps"):
178
- # Tools were called
179
- tool_calls = []
180
- for action, _ in result["intermediate_steps"]:
181
- if hasattr(action, "tool") and hasattr(action, "tool_input"):
182
- tool_calls.append(
183
- MCPToolCall(
184
- name=action.tool,
185
- arguments=action.tool_input,
186
- )
187
- )
188
-
189
- return ModelResponse(content=output, tool_calls=tool_calls, done=False)
190
- else:
191
- # No tools called, just text response
192
- return ModelResponse(content=output, tool_calls=[], done=True)
193
-
194
- except Exception as e:
195
- logger.error("Agent execution failed: %s", e)
196
- return ModelResponse(content=f"Error: {e!s}", tool_calls=[], done=True)
197
-
198
- async def format_tool_results(
199
- self, tool_calls: list[MCPToolCall], tool_results: list[MCPToolResult]
200
- ) -> list[BaseMessage]:
201
- """Format tool results into LangChain messages."""
202
- # Create an AI message with the tool calls and results
203
- messages = []
204
-
205
- # First add an AI message indicating tools were called
206
- tool_names = [tc.name for tc in tool_calls]
207
- ai_content = f"I'll use the following tools: {', '.join(tool_names)}"
208
- messages.append(AIMessage(content=ai_content))
209
-
210
- # Build result text from tool results
211
- text_parts = []
212
- latest_screenshot = None
213
-
214
- for tool_call, result in zip(tool_calls, tool_results, strict=False):
215
- if result.isError:
216
- error_text = "Tool execution failed"
217
- for content in result.content:
218
- if isinstance(content, types.TextContent):
219
- error_text = content.text
220
- break
221
- text_parts.append(f"Error - {tool_call.name}: {error_text}")
222
- else:
223
- # Process success content
224
- tool_output = []
225
- for content in result.content:
226
- if isinstance(content, types.TextContent):
227
- tool_output.append(content.text)
228
- elif isinstance(content, types.ImageContent):
229
- latest_screenshot = content.data
230
-
231
- if tool_output:
232
- text_parts.append(f"{tool_call.name}: " + " ".join(tool_output))
233
-
234
- result_text = "\n".join(text_parts) if text_parts else "No output from tools"
235
-
236
- # Then add a human message with the tool results
237
- if latest_screenshot:
238
- # Include screenshot in multimodal format
239
- content = [
240
- {"type": "text", "text": f"Tool results:\n{result_text}"},
241
- {
242
- "type": "image_url",
243
- "image_url": {"url": f"data:image/png;base64,{latest_screenshot}"},
244
- },
245
- ]
246
- messages.append(HumanMessage(content=content))
247
- else:
248
- messages.append(HumanMessage(content=f"Tool results:\n{result_text}"))
249
-
250
- return messages
1
+ """LangChain MCP Agent implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any, ClassVar
7
+
8
+ import mcp.types as types
9
+ from langchain.agents import AgentExecutor, create_tool_calling_agent
10
+ from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
11
+ from langchain.schema import AIMessage, BaseMessage, HumanMessage, SystemMessage
12
+ from mcp_use.adapters.langchain_adapter import LangChainAdapter
13
+
14
+ import hud
15
+
16
+ if TYPE_CHECKING:
17
+ from langchain.schema.language_model import BaseLanguageModel
18
+ from langchain_core.tools import BaseTool
19
+
20
+ from hud.types import AgentResponse, MCPToolCall, MCPToolResult
21
+
22
+ from .base import MCPAgent
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class LangChainAgent(MCPAgent):
28
+ """
29
+ LangChain agent that uses MCP servers for tool execution.
30
+
31
+ This agent wraps any LangChain-compatible LLM and provides
32
+ access to MCP tools through LangChain's tool-calling interface.
33
+ """
34
+
35
+ metadata: ClassVar[dict[str, Any]] = {
36
+ "display_width": 1920,
37
+ "display_height": 1080,
38
+ }
39
+
40
+ def __init__(
41
+ self,
42
+ llm: BaseLanguageModel,
43
+ **kwargs: Any,
44
+ ) -> None:
45
+ """
46
+ Initialize LangChain MCP agent.
47
+
48
+ Args:
49
+ llm: Any LangChain-compatible language model
50
+ **kwargs: Additional arguments passed to BaseMCPAgent
51
+ """
52
+ super().__init__(**kwargs)
53
+
54
+ self.llm = llm
55
+ self.adapter = LangChainAdapter(disallowed_tools=self.disallowed_tools)
56
+ self._langchain_tools: list[BaseTool] | None = None
57
+
58
+ self.model_name = (
59
+ "langchain-" + self.llm.model_name # type: ignore
60
+ if hasattr(self.llm, "model_name")
61
+ else "unknown"
62
+ )
63
+
64
+ def _get_langchain_tools(self) -> list[BaseTool]:
65
+ """Get or create LangChain tools from MCP tools."""
66
+ if self._langchain_tools is not None:
67
+ return self._langchain_tools
68
+
69
+ # Create LangChain tools from MCP tools using the adapter
70
+ self._langchain_tools = []
71
+
72
+ # Convert available tools using the adapter; no server grouping
73
+ langchain_tools = self.adapter._convert_tools(self._available_tools, "default") # type: ignore[reportAttributeAccessIssue]
74
+ self._langchain_tools.extend(langchain_tools)
75
+
76
+ logger.info("Created %s LangChain tools from MCP tools", len(self._langchain_tools))
77
+ return self._langchain_tools
78
+
79
+ async def get_system_messages(self) -> list[BaseMessage]:
80
+ """Get system messages for LangChain."""
81
+ return [SystemMessage(content=self.system_prompt)]
82
+
83
+ async def format_blocks(self, blocks: list[types.ContentBlock]) -> list[BaseMessage]:
84
+ """Create initial messages for LangChain."""
85
+ messages = []
86
+ for block in blocks:
87
+ if isinstance(block, types.TextContent):
88
+ messages.append(HumanMessage(content=block.text))
89
+ elif isinstance(block, types.ImageContent):
90
+ messages.append(HumanMessage(content=block.data))
91
+ return messages
92
+
93
+ @hud.instrument(
94
+ span_type="agent",
95
+ record_args=False, # Messages can be large
96
+ record_result=True,
97
+ )
98
+ async def get_response(self, messages: list[BaseMessage]) -> AgentResponse:
99
+ """Get response from LangChain model including any tool calls."""
100
+ # Get LangChain tools (created lazily)
101
+ langchain_tools = self._get_langchain_tools()
102
+
103
+ # Create a prompt template from current messages
104
+ # Extract system message if present
105
+ system_content = "You are a helpful assistant"
106
+ non_system_messages = []
107
+
108
+ for msg in messages:
109
+ if isinstance(msg, SystemMessage):
110
+ system_content = str(msg.content)
111
+ else:
112
+ non_system_messages.append(msg)
113
+
114
+ # Create prompt with placeholders
115
+ prompt = ChatPromptTemplate.from_messages(
116
+ [
117
+ ("system", system_content),
118
+ MessagesPlaceholder(variable_name="chat_history"),
119
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
120
+ ]
121
+ )
122
+
123
+ # Create agent with tools
124
+ agent = create_tool_calling_agent(
125
+ llm=self.llm,
126
+ tools=langchain_tools,
127
+ prompt=prompt,
128
+ )
129
+
130
+ # Create executor
131
+ executor = AgentExecutor(
132
+ agent=agent,
133
+ tools=langchain_tools,
134
+ verbose=False,
135
+ )
136
+
137
+ # Format the last user message as input
138
+ last_user_msg = None
139
+ for msg in reversed(non_system_messages):
140
+ if isinstance(msg, HumanMessage):
141
+ last_user_msg = msg
142
+ break
143
+
144
+ if not last_user_msg:
145
+ return AgentResponse(content="No user message found", tool_calls=[], done=True)
146
+
147
+ # Extract text from message content
148
+ input_text = ""
149
+ if isinstance(last_user_msg.content, str):
150
+ input_text = last_user_msg.content
151
+ elif isinstance(last_user_msg.content, list):
152
+ # Extract text from multimodal content
153
+ for item in last_user_msg.content:
154
+ if isinstance(item, dict) and item.get("type") == "text":
155
+ input_text = item.get("text", "")
156
+ break
157
+
158
+ # Build chat history (exclude last user message and system)
159
+ chat_history = []
160
+ for _, msg in enumerate(non_system_messages[:-1]):
161
+ if isinstance(msg, HumanMessage | AIMessage):
162
+ chat_history.append(msg)
163
+
164
+ # Execute the agent
165
+ try:
166
+ result = await executor.ainvoke(
167
+ {
168
+ "input": input_text,
169
+ "chat_history": chat_history,
170
+ }
171
+ )
172
+
173
+ # Process the result
174
+ output = result.get("output", "")
175
+
176
+ # Check if tools were called
177
+ if result.get("intermediate_steps"):
178
+ # Tools were called
179
+ tool_calls = []
180
+ for action, _ in result["intermediate_steps"]:
181
+ if hasattr(action, "tool") and hasattr(action, "tool_input"):
182
+ tool_calls.append(
183
+ MCPToolCall(
184
+ name=action.tool,
185
+ arguments=action.tool_input,
186
+ )
187
+ )
188
+
189
+ return AgentResponse(content=output, tool_calls=tool_calls, done=False)
190
+ else:
191
+ # No tools called, just text response
192
+ return AgentResponse(content=output, tool_calls=[], done=True)
193
+
194
+ except Exception as e:
195
+ logger.error("Agent execution failed: %s", e)
196
+ return AgentResponse(content=f"Error: {e!s}", tool_calls=[], done=True)
197
+
198
+ async def format_tool_results(
199
+ self, tool_calls: list[MCPToolCall], tool_results: list[MCPToolResult]
200
+ ) -> list[BaseMessage]:
201
+ """Format tool results into LangChain messages."""
202
+ # Create an AI message with the tool calls and results
203
+ messages = []
204
+
205
+ # First add an AI message indicating tools were called
206
+ tool_names = [tc.name for tc in tool_calls]
207
+ ai_content = f"I'll use the following tools: {', '.join(tool_names)}"
208
+ messages.append(AIMessage(content=ai_content))
209
+
210
+ # Build result text from tool results
211
+ text_parts = []
212
+ latest_screenshot = None
213
+
214
+ for tool_call, result in zip(tool_calls, tool_results, strict=False):
215
+ if result.isError:
216
+ error_text = "Tool execution failed"
217
+ for content in result.content:
218
+ if isinstance(content, types.TextContent):
219
+ error_text = content.text
220
+ break
221
+ text_parts.append(f"Error - {tool_call.name}: {error_text}")
222
+ else:
223
+ # Process success content
224
+ tool_output = []
225
+ for content in result.content:
226
+ if isinstance(content, types.TextContent):
227
+ tool_output.append(content.text)
228
+ elif isinstance(content, types.ImageContent):
229
+ latest_screenshot = content.data
230
+
231
+ if tool_output:
232
+ text_parts.append(f"{tool_call.name}: " + " ".join(tool_output))
233
+
234
+ result_text = "\n".join(text_parts) if text_parts else "No output from tools"
235
+
236
+ # Then add a human message with the tool results
237
+ if latest_screenshot:
238
+ # Include screenshot in multimodal format
239
+ content = [
240
+ {"type": "text", "text": f"Tool results:\n{result_text}"},
241
+ {
242
+ "type": "image_url",
243
+ "image_url": {"url": f"data:image/png;base64,{latest_screenshot}"},
244
+ },
245
+ ]
246
+ messages.append(HumanMessage(content=content))
247
+ else:
248
+ messages.append(HumanMessage(content=f"Tool results:\n{result_text}"))
249
+
250
+ return messages
@@ -0,0 +1,7 @@
1
+ """Miscellaneous agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .response_agent import ResponseAgent
6
+
7
+ __all__ = ["ResponseAgent"]