universal-mcp-agents 0.1.23__py3-none-any.whl → 0.1.24rc3__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.
Files changed (35) hide show
  1. universal_mcp/agents/__init__.py +11 -2
  2. universal_mcp/agents/base.py +3 -6
  3. universal_mcp/agents/codeact0/agent.py +14 -17
  4. universal_mcp/agents/codeact0/prompts.py +9 -3
  5. universal_mcp/agents/codeact0/sandbox.py +2 -2
  6. universal_mcp/agents/codeact0/tools.py +2 -2
  7. universal_mcp/agents/codeact0/utils.py +48 -0
  8. universal_mcp/agents/codeact00/__init__.py +3 -0
  9. universal_mcp/agents/codeact00/__main__.py +26 -0
  10. universal_mcp/agents/codeact00/agent.py +578 -0
  11. universal_mcp/agents/codeact00/config.py +77 -0
  12. universal_mcp/agents/codeact00/langgraph_agent.py +14 -0
  13. universal_mcp/agents/codeact00/llm_tool.py +25 -0
  14. universal_mcp/agents/codeact00/prompts.py +364 -0
  15. universal_mcp/agents/codeact00/sandbox.py +135 -0
  16. universal_mcp/agents/codeact00/state.py +66 -0
  17. universal_mcp/agents/codeact00/tools.py +525 -0
  18. universal_mcp/agents/codeact00/utils.py +678 -0
  19. universal_mcp/agents/codeact01/__init__.py +3 -0
  20. universal_mcp/agents/codeact01/__main__.py +26 -0
  21. universal_mcp/agents/codeact01/agent.py +413 -0
  22. universal_mcp/agents/codeact01/config.py +77 -0
  23. universal_mcp/agents/codeact01/langgraph_agent.py +14 -0
  24. universal_mcp/agents/codeact01/llm_tool.py +25 -0
  25. universal_mcp/agents/codeact01/prompts.py +246 -0
  26. universal_mcp/agents/codeact01/sandbox.py +162 -0
  27. universal_mcp/agents/codeact01/state.py +58 -0
  28. universal_mcp/agents/codeact01/tools.py +648 -0
  29. universal_mcp/agents/codeact01/utils.py +552 -0
  30. universal_mcp/agents/llm.py +7 -3
  31. universal_mcp/applications/llm/app.py +66 -15
  32. {universal_mcp_agents-0.1.23.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/METADATA +1 -1
  33. universal_mcp_agents-0.1.24rc3.dist-info/RECORD +66 -0
  34. universal_mcp_agents-0.1.23.dist-info/RECORD +0 -44
  35. {universal_mcp_agents-0.1.23.dist-info → universal_mcp_agents-0.1.24rc3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,26 @@
1
+ import asyncio
2
+
3
+ from langgraph.checkpoint.memory import MemorySaver
4
+ from rich import print
5
+ from universal_mcp.agentr.registry import AgentrRegistry
6
+
7
+ from universal_mcp.agents.codeact01.agent import CodeActPlaybookAgent
8
+ from universal_mcp.agents.utils import messages_to_list
9
+
10
+
11
+ async def main():
12
+ memory = MemorySaver()
13
+ agent = CodeActPlaybookAgent(
14
+ name="CodeAct Agent",
15
+ instructions="Be very concise in your answers.",
16
+ model="azure/gpt-4.1",
17
+ registry=AgentrRegistry(),
18
+ memory=memory,
19
+ )
20
+ print("Starting agent...")
21
+ result = await agent.invoke(user_input="Check my google calendar and show my todays agenda")
22
+ print(messages_to_list(result["messages"]))
23
+
24
+
25
+ if __name__ == "__main__":
26
+ asyncio.run(main())
@@ -0,0 +1,413 @@
1
+ import copy
2
+ import json
3
+ import re
4
+ import uuid
5
+ from typing import Literal, cast
6
+
7
+ from langchain_anthropic import ChatAnthropic
8
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
9
+ from langgraph.checkpoint.base import BaseCheckpointSaver
10
+ from langgraph.graph import START, StateGraph
11
+ from langgraph.types import Command, RetryPolicy, StreamWriter
12
+ from universal_mcp.tools.registry import ToolRegistry
13
+ from universal_mcp.types import ToolFormat
14
+
15
+ from universal_mcp.agents.base import BaseAgent
16
+ from universal_mcp.agents.codeact01.llm_tool import smart_print
17
+ from universal_mcp.agents.codeact01.prompts import (
18
+ AGENT_BUILDER_GENERATING_PROMPT,
19
+ AGENT_BUILDER_META_PROMPT,
20
+ AGENT_BUILDER_PLANNING_PROMPT,
21
+ build_tool_definitions,
22
+ create_default_prompt,
23
+ )
24
+ from universal_mcp.agents.codeact01.sandbox import eval_unsafe, execute_ipython_cell, handle_execute_ipython_cell
25
+ from universal_mcp.agents.codeact01.state import AgentBuilderCode, AgentBuilderMeta, AgentBuilderPlan, CodeActState
26
+ from universal_mcp.agents.codeact01.tools import (
27
+ create_agent_builder_tools,
28
+ create_meta_tools,
29
+ enter_agent_builder_mode,
30
+ )
31
+ from universal_mcp.agents.codeact01.utils import build_anthropic_cache_message, extract_plan_parameters, get_connected_apps_string, strip_thinking
32
+ from universal_mcp.agents.llm import load_chat_model
33
+ from universal_mcp.agents.utils import convert_tool_ids_to_dict, filter_retry_on, get_message_text
34
+
35
+
36
+ class CodeActPlaybookAgent(BaseAgent):
37
+ def __init__(
38
+ self,
39
+ name: str,
40
+ instructions: str,
41
+ model: str,
42
+ memory: BaseCheckpointSaver | None = None,
43
+ registry: ToolRegistry | None = None,
44
+ agent_builder_registry: object | None = None,
45
+ sandbox_timeout: int = 20,
46
+ **kwargs,
47
+ ):
48
+ super().__init__(
49
+ name=name,
50
+ instructions=instructions,
51
+ model=model,
52
+ memory=memory,
53
+ **kwargs,
54
+ )
55
+ self.model_instance = load_chat_model(model)
56
+ self.agent_builder_model_instance = load_chat_model("anthropic:claude-sonnet-4-5-20250929", thinking=False)
57
+ self.registry = registry
58
+ self.agent_builder_registry = agent_builder_registry
59
+ self.agent = agent_builder_registry.get_agent() if agent_builder_registry else None
60
+
61
+ self.tools_config = self.agent.tools if self.agent else {}
62
+ self.eval_fn = eval_unsafe
63
+ self.sandbox_timeout = sandbox_timeout
64
+ self.default_tools_config = {
65
+ "llm": ["generate_text", "classify_data", "extract_data", "call_llm"],
66
+ }
67
+ self.final_instructions = ""
68
+ self.tools_context = {}
69
+ self.eval_mode = kwargs.get("eval_mode", False)
70
+
71
+ async def handle_agent_save(
72
+ self,
73
+ meta_id: str,
74
+ agent_name: str,
75
+ agent_description: str,
76
+ python_code: str,
77
+ writer: StreamWriter,
78
+ state: CodeActState,
79
+ effective_previous_add_context: dict,
80
+ ) -> tuple[str, dict]:
81
+ """Handle saving agent code and updating the UI."""
82
+ if self.agent:
83
+ # Update flow: use existing name/description and do not re-generate
84
+ agent_name = getattr(self.agent, "name", None)
85
+ agent_description = getattr(self.agent, "description", None)
86
+ writer(
87
+ {
88
+ "type": "custom",
89
+ id: meta_id,
90
+ "name": "generating",
91
+ "data": {
92
+ "update": True,
93
+ "name": agent_name,
94
+ "description": agent_description,
95
+ },
96
+ }
97
+ )
98
+ tool_result = "Successfully updated the existing agent."
99
+ else:
100
+ # Emit intermediary UI update with created name/description
101
+ writer(
102
+ {
103
+ "type": "custom",
104
+ id: meta_id,
105
+ "name": "generating",
106
+ "data": {"update": False, "name": agent_name, "description": agent_description},
107
+ }
108
+ )
109
+ res_id = str(uuid.uuid4())
110
+
111
+ try:
112
+ if not self.agent_builder_registry:
113
+ raise ValueError("AgentBuilder registry is not configured")
114
+
115
+ plan_params = extract_plan_parameters(state["plan"])
116
+
117
+ # Build instructions payload embedding the plan and function code
118
+ instructions_payload = {
119
+ "plan": state["plan"],
120
+ "script": python_code,
121
+ "params": plan_params,
122
+ }
123
+
124
+ # Convert tool ids list to dict
125
+ tool_dict = convert_tool_ids_to_dict(state["selected_tool_ids"])
126
+
127
+ res = self.agent_builder_registry.upsert_agent(
128
+ name=agent_name,
129
+ description=agent_description,
130
+ instructions=instructions_payload,
131
+ tools=tool_dict,
132
+ )
133
+ res_id = str(res.id)
134
+ tool_result = "Succesfully saved the agent plan and code."
135
+ writer(
136
+ {
137
+ "type": "custom",
138
+ id: meta_id,
139
+ "name": "generating",
140
+ "data": {
141
+ "id": str(res.id),
142
+ "update": bool(self.agent),
143
+ "name": agent_name,
144
+ "description": agent_description,
145
+ "add_context": effective_previous_add_context,
146
+ },
147
+ }
148
+ )
149
+ except Exception:
150
+ # In case of error, add the code to the exit message content
151
+ tool_result = f"Displaying the final saved code:\n\n{python_code}\nFinal Name: {agent_name}\nDescription: {agent_description}"
152
+
153
+ if "functions" not in effective_previous_add_context:
154
+ effective_previous_add_context["functions"] = []
155
+ effective_previous_add_context["functions"].append(python_code)
156
+
157
+ return tool_result, effective_previous_add_context, res_id
158
+
159
+ async def _build_graph(self): # noqa: PLR0915
160
+ """Build the graph for the CodeAct Playbook Agent."""
161
+ meta_tools = create_meta_tools(self.registry)
162
+ agent_builder_tools = create_agent_builder_tools()
163
+ self.additional_tools = [
164
+ smart_print,
165
+ meta_tools["web_search"],
166
+ meta_tools["read_file"],
167
+ meta_tools["save_file"],
168
+ meta_tools["upload_file"],
169
+ ]
170
+
171
+ if self.tools_config:
172
+ await self.registry.load_tools(self.tools_config) # Load provided tools
173
+ if self.default_tools_config:
174
+ await self.registry.load_tools(self.default_tools_config) # Load default tools
175
+
176
+ async def call_model(state: CodeActState) -> Command[Literal["execute_tools"]]:
177
+ """This node now only ever binds the four meta-tools to the LLM."""
178
+ messages = build_anthropic_cache_message(self.final_instructions) + state["messages"]
179
+ agent_facing_tools = [
180
+ execute_ipython_cell,
181
+ # enter_agent_builder_mode,
182
+ agent_builder_tools["create_agent_plan"],
183
+ agent_builder_tools["modify_agent_plan"],
184
+ agent_builder_tools["save_agent_code"],
185
+ meta_tools["search_functions"],
186
+ meta_tools["load_functions"],
187
+ ]
188
+
189
+ if isinstance(self.model_instance, ChatAnthropic):
190
+ model_with_tools = self.model_instance.bind_tools(
191
+ tools=agent_facing_tools,
192
+ tool_choice="auto",
193
+ cache_control={"type": "ephemeral", "ttl": "1h"},
194
+ )
195
+ if isinstance(messages[-1].content, str):
196
+ pass
197
+ else:
198
+ last = copy.deepcopy(messages[-1])
199
+ last.content[-1]["cache_control"] = {"type": "ephemeral", "ttl": "5m"}
200
+ messages[-1] = last
201
+ else:
202
+ model_with_tools = self.model_instance.bind_tools(
203
+ tools=agent_facing_tools,
204
+ tool_choice="auto",
205
+ )
206
+ response = cast(AIMessage, await model_with_tools.ainvoke(messages))
207
+ if response.tool_calls:
208
+ return Command(goto="execute_tools", update={"messages": [response]})
209
+ else:
210
+ return Command(update={"messages": [response], "model_with_tools": model_with_tools})
211
+
212
+ async def execute_tools(state: CodeActState, writer: StreamWriter) -> Command[Literal["call_model"]]:
213
+ """Execute tool calls"""
214
+ last_message = state["messages"][-1]
215
+ tool_calls = last_message.tool_calls if isinstance(last_message, AIMessage) else []
216
+
217
+ tool_messages = []
218
+ new_tool_ids = []
219
+ tool_result = ""
220
+ ask_user = False
221
+ ai_msg = ""
222
+ additional_kwargs = {}
223
+ effective_previous_add_context = state.get("add_context", {})
224
+ effective_existing_context = state.get("context", {})
225
+ plan = state.get("plan", None)
226
+ agent_name = state.get("agent_name", None)
227
+ agent_description = state.get("agent_description", None)
228
+ # logging.info(f"Initial new_tool_ids_for_context: {new_tool_ids_for_context}")
229
+
230
+ for tool_call in tool_calls:
231
+ tool_name = tool_call["name"]
232
+ tool_args = tool_call["args"]
233
+ try:
234
+ if tool_name == "execute_ipython_cell":
235
+ code = tool_call["args"]["snippet"]
236
+ output, new_context, new_add_context = await handle_execute_ipython_cell(
237
+ code,
238
+ self.tools_context, # Uses the dynamically updated context
239
+ self.eval_fn,
240
+ effective_previous_add_context,
241
+ effective_existing_context,
242
+ )
243
+ effective_existing_context = new_context
244
+ effective_previous_add_context = new_add_context
245
+ tool_result = output
246
+ elif tool_name == "load_functions":
247
+ # The tool now does all the work of validation and formatting.
248
+ tool_result, new_context_for_sandbox, valid_tools, unconnected_links = await meta_tools[
249
+ "load_functions"
250
+ ].ainvoke(tool_args)
251
+ # We still need to update the sandbox context for `execute_ipython_cell`
252
+ new_tool_ids.extend(valid_tools)
253
+ if new_tool_ids:
254
+ self.tools_context.update(new_context_for_sandbox)
255
+ if unconnected_links:
256
+ ask_user = True
257
+ ai_msg = f"Please login to the following app(s) using the following links and let me know in order to proceed:\n {unconnected_links} "
258
+
259
+ elif tool_name == "search_functions":
260
+ tool_result = await meta_tools["search_functions"].ainvoke(tool_args)
261
+
262
+ elif tool_name == "create_agent_plan":
263
+ plan_id = str(uuid.uuid4())
264
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": False}})
265
+ ask_user = True
266
+ tool_result = "The user has been shown the plan. Proceed according to their next message"
267
+ plan = await agent_builder_tools["create_agent_plan"].ainvoke(tool_args)
268
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan}})
269
+ additional_kwargs = {"type": "planning", "plan": plan, "update": False}
270
+ elif tool_name =="modify_agent_plan":
271
+ if plan is None:
272
+ tool_result = "You must have an existing agent plan or created one using create_agent_plan before calling modify_agent_plan."
273
+ else:
274
+ plan_id = str(uuid.uuid4())
275
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"update": True}})
276
+ # Apply modifications to the existing plan per docstring semantics
277
+ modifications = await agent_builder_tools["modify_agent_plan"].ainvoke(tool_args)
278
+ new_plan = []
279
+ old_idx = 0
280
+ total_old = len(plan)
281
+ for step in modifications:
282
+ s = step.strip() if isinstance(step, str) else ""
283
+ if not s:
284
+ continue
285
+ if s == "<nochange>":
286
+ if old_idx < total_old:
287
+ new_plan.append(plan[old_idx])
288
+ old_idx += 1
289
+ continue
290
+ if s == "<delete>":
291
+ old_idx += 1
292
+ continue
293
+ if s.startswith("<new>") and s.endswith("</new>"):
294
+ content = s[len("<new>"):-len("</new>")].strip()
295
+ if content:
296
+ new_plan.append(content)
297
+ continue
298
+ if s.startswith("<modify>") and s.endswith("</modify>"):
299
+ content = s[len("<modify>"):-len("</modify>")].strip()
300
+ if content:
301
+ new_plan.append(content)
302
+ # consume the old step being modified
303
+ old_idx += 1
304
+ continue
305
+ # Ignore unknown directives when modifying
306
+
307
+ # Append any remaining old steps not explicitly addressed by modifications
308
+ if old_idx < total_old:
309
+ new_plan.extend(plan[old_idx:])
310
+
311
+ plan = new_plan
312
+ writer({"type": "custom", id: plan_id, "name": "planning", "data": {"plan": plan}})
313
+ additional_kwargs = {"type": "planning", "plan": plan, "update": True}
314
+ ask_user = True
315
+ tool_result = "The user has been shown the plan. Proceed according to their next message."
316
+
317
+ elif tool_name == "save_agent_code":
318
+ self.meta_id = str(uuid.uuid4())
319
+ writer({"type": "custom", id: self.meta_id, "name": "generating", "data": {"update": bool(self.agent)}})
320
+ agent_name, agent_description, python_code = await agent_builder_tools["save_agent_code"].ainvoke(tool_args)
321
+ tool_result, effective_previous_add_context, agent_id = await self.handle_agent_save(
322
+ meta_id=self.meta_id,
323
+ agent_name=agent_name,
324
+ agent_description=agent_description,
325
+ python_code=python_code,
326
+ writer=writer,
327
+ state=state,
328
+ effective_previous_add_context=effective_previous_add_context,
329
+ )
330
+
331
+
332
+ else:
333
+ raise Exception(
334
+ f"Unexpected tool call: {tool_call['name']}. "
335
+ "tool calls must be one of 'enter_agent_builder_mode', 'execute_ipython_cell', 'load_functions', 'search_functions', 'create_agent_plan', or 'save_agent_code'. For using functions, call them in code using 'execute_ipython_cell'."
336
+ )
337
+
338
+
339
+
340
+
341
+ except Exception as e:
342
+ tool_result = str(e)
343
+
344
+ tool_message = ToolMessage(
345
+ content=json.dumps(tool_result),
346
+ name=tool_call["name"],
347
+ tool_call_id=tool_call["id"],
348
+ )
349
+ tool_messages.append(tool_message)
350
+
351
+ if ask_user:
352
+ tool_messages.append(AIMessage(content=ai_msg, additional_kwargs=additional_kwargs))
353
+ return Command(
354
+ update={
355
+ "messages": tool_messages,
356
+ "selected_tool_ids": new_tool_ids,
357
+ "context": effective_existing_context,
358
+ "add_context": effective_previous_add_context,
359
+ "agent_name": agent_name,
360
+ "agent_description": agent_description,
361
+ "plan": plan,
362
+ }
363
+ )
364
+
365
+ return Command(
366
+ goto="call_model",
367
+ update={
368
+ "messages": tool_messages,
369
+ "selected_tool_ids": new_tool_ids,
370
+ "context": effective_existing_context,
371
+ "add_context": effective_previous_add_context,
372
+ "agent_name": agent_name,
373
+ "agent_description": agent_description,
374
+ "plan": plan,
375
+ },
376
+ )
377
+
378
+ async def route_entry(state: CodeActState) -> Command[Literal["call_model", "execute_tools"]]:
379
+ """Route to either normal mode or agent builder creation"""
380
+ pre_tools = await self.registry.export_tools(format=ToolFormat.NATIVE)
381
+
382
+ # Create the initial system prompt and tools_context in one go
383
+ self.final_instructions, self.tools_context = create_default_prompt(
384
+ pre_tools,
385
+ self.additional_tools,
386
+ self.instructions,
387
+ await get_connected_apps_string(self.registry),
388
+ self.agent,
389
+ is_initial_prompt=True,
390
+ )
391
+ self.preloaded_defs, _ = build_tool_definitions(pre_tools)
392
+ self.preloaded_defs = "\n".join(self.preloaded_defs)
393
+ await self.registry.load_tools(state["selected_tool_ids"])
394
+ exported_tools = await self.registry.export_tools(
395
+ state["selected_tool_ids"], ToolFormat.NATIVE
396
+ ) # Get definition for only the new tools
397
+ _, loaded_tools_context = build_tool_definitions(exported_tools)
398
+ self.tools_context.update(loaded_tools_context)
399
+
400
+ if (
401
+ len(state["messages"]) == 1 and self.agent
402
+ ): # Inject the agent's script function into add_context for execution
403
+ script = self.agent.instructions.get("script")
404
+ add_context = {"functions": [script]}
405
+ return Command(goto="call_model", update={"add_context": add_context})
406
+ return Command(goto="call_model")
407
+
408
+ agent = StateGraph(state_schema=CodeActState)
409
+ agent.add_node(call_model, retry_policy=RetryPolicy(max_attempts=3, retry_on=filter_retry_on))
410
+ agent.add_node(execute_tools)
411
+ agent.add_node(route_entry)
412
+ agent.add_edge(START, "route_entry")
413
+ return agent.compile(checkpointer=self.memory)
@@ -0,0 +1,77 @@
1
+ from typing import Annotated, Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ # Literal type for all available usecase filenames
6
+ UseCaseName = Literal[
7
+ " ",
8
+ "1-unsubscribe",
9
+ "2-reddit",
10
+ "2.1-reddit",
11
+ "3-earnings",
12
+ "4-maps",
13
+ "4.1-maps",
14
+ "5-gmailreply",
15
+ "6-contract",
16
+ "7-overnight",
17
+ "8-sheets_chart",
18
+ "9-learning",
19
+ "10-reddit2",
20
+ "11-github",
21
+ ]
22
+
23
+
24
+ class ContextSchema(BaseModel):
25
+ """The configuration for the agent."""
26
+
27
+ base_prompt: str = Field(
28
+ default=" ",
29
+ description="The base prompt to use for the agent's interactions. Leave blank if using a JSON prompt from the dropdown.",
30
+ )
31
+ model_provider: Annotated[
32
+ Literal[
33
+ "openai",
34
+ "anthropic",
35
+ "azure_openai",
36
+ "azure_ai",
37
+ "google_vertexai",
38
+ "google_genai",
39
+ "bedrock",
40
+ "bedrock_converse",
41
+ "cohere",
42
+ "fireworks",
43
+ "together",
44
+ "mistralai",
45
+ "huggingface",
46
+ "groq",
47
+ "ollama",
48
+ "google_anthropic_vertex",
49
+ "deepseek",
50
+ "ibm",
51
+ "nvidia",
52
+ "xai",
53
+ "perplexity",
54
+ ],
55
+ {"__template_metadata__": {"kind": "provider"}},
56
+ ] = Field(
57
+ default="anthropic",
58
+ description="The name of the model provider to use for the agent's main interactions. ",
59
+ )
60
+ model: Annotated[
61
+ Literal[
62
+ "claude-4-sonnet-20250514",
63
+ "claude-sonnet-4@20250514",
64
+ ],
65
+ {"__template_metadata__": {"kind": "llm"}},
66
+ ] = Field(
67
+ default="claude-4-sonnet-20250514",
68
+ description="The name of the language model to use for the agent's main interactions. ",
69
+ )
70
+ tool_names: list[str] = Field(
71
+ default=[],
72
+ description="The names of the tools to use for the agent's main interactions. Leave blank if using a JSON prompt from the dropdown.",
73
+ )
74
+ json_prompt_name: UseCaseName = Field(
75
+ default=" ",
76
+ description="The name of the JSON prompt to use for the agent's main interactions, instead of providing a base prompt and tool names. ",
77
+ )
@@ -0,0 +1,14 @@
1
+ from universal_mcp.agentr.registry import AgentrRegistry
2
+
3
+ from universal_mcp.agents.codeact01 import CodeActPlaybookAgent
4
+
5
+
6
+ async def agent():
7
+ agent_obj = CodeActPlaybookAgent(
8
+ name="CodeAct Agent",
9
+ instructions="Be very concise in your answers.",
10
+ model="anthropic:claude-4-sonnet-20250514",
11
+ tools=[],
12
+ registry=AgentrRegistry(),
13
+ )
14
+ return await agent_obj._build_graph()
@@ -0,0 +1,25 @@
1
+ from typing import Any
2
+
3
+ from universal_mcp.agents.codeact01.utils import light_copy
4
+
5
+ MAX_RETRIES = 3
6
+
7
+
8
+ def get_context_str(source: Any | list[Any] | dict[str, Any]) -> str:
9
+ """Converts context to a string representation."""
10
+ if not isinstance(source, dict):
11
+ if isinstance(source, list):
12
+ source = {f"doc_{i + 1}": str(doc) for i, doc in enumerate(source)}
13
+ else:
14
+ source = {"content": str(source)}
15
+
16
+ return "\n".join(f"<{k}>\n{str(v)}\n</{k}>" for k, v in source.items())
17
+
18
+
19
+ def smart_print(data: Any) -> None:
20
+ """Prints a dictionary or list of dictionaries with string values truncated to 30 characters.
21
+
22
+ Args:
23
+ data: Either a dictionary with string keys, or a list of such dictionaries
24
+ """
25
+ print(light_copy(data)) # noqa: T201