From fd312211fffcd8be98ce6458af6615e6e2ff71ae Mon Sep 17 00:00:00 2001 From: daavoo Date: Wed, 18 Feb 2026 16:06:21 +0100 Subject: [PATCH] feat(tinyagent): Allow messages as input to agent.run. --- src/any_agent/frameworks/agno.py | 7 +++++- src/any_agent/frameworks/any_agent.py | 18 +++++++++++---- src/any_agent/frameworks/google.py | 5 +++- src/any_agent/frameworks/langchain.py | 7 +++++- src/any_agent/frameworks/llama_index.py | 7 +++++- src/any_agent/frameworks/openai.py | 7 +++++- src/any_agent/frameworks/smolagents.py | 7 +++++- src/any_agent/frameworks/tinyagent.py | 27 +++++++++++++--------- tests/integration/frameworks/test_agent.py | 17 ++++++++++++++ 9 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/any_agent/frameworks/agno.py b/src/any_agent/frameworks/agno.py index 027b0284..2b68ddfe 100644 --- a/src/any_agent/frameworks/agno.py +++ b/src/any_agent/frameworks/agno.py @@ -278,10 +278,15 @@ async def _load_agent(self) -> None: **agent_args, ) - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: if not self._agent: error_message = "Agent not loaded. Call load_agent() first." raise ValueError(error_message) + if not isinstance(prompt, str): + msg = "Agno framework does not support list of messages as input. Use a plain string prompt." + raise NotImplementedError(msg) result: RunResponse = await self._agent.arun(prompt, **kwargs) return result.content # type: ignore[return-value] diff --git a/src/any_agent/frameworks/any_agent.py b/src/any_agent/frameworks/any_agent.py index a6f79b5b..39692c62 100644 --- a/src/any_agent/frameworks/any_agent.py +++ b/src/any_agent/frameworks/any_agent.py @@ -314,17 +314,25 @@ async def __aexit__( """Exit the async context manager and clean up resources.""" await self.cleanup_async() - def run(self, prompt: str, **kwargs: Any) -> AgentTrace: + def run(self, prompt: str | list[dict[str, Any]], **kwargs: Any) -> AgentTrace: """Run the agent with the given prompt.""" return run_async_in_sync( self.run_async(prompt, **kwargs), allow_running_loop=INSIDE_NOTEBOOK ) - async def run_async(self, prompt: str, **kwargs: Any) -> AgentTrace: + async def run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> AgentTrace: """Run the agent asynchronously with the given prompt. Args: - prompt: The user prompt to be passed to the agent. + prompt: The user prompt to be passed to the agent. Can be a plain + string or a list of message dicts (e.g. + ``[{"role": "user", "content": "hello"}]``) following the + OpenAI chat-completion message format. When a list is provided + it is forwarded directly to the underlying LLM, giving callers + full control over the conversation structure. + Note: passing a list is only supported by the ``TINYAGENT`` framework. kwargs: Will be passed to the underlying runner used by the framework. @@ -530,7 +538,9 @@ async def _load_agent(self) -> None: """Load the agent instance.""" @abstractmethod - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: """To be implemented by each framework.""" @abstractmethod diff --git a/src/any_agent/frameworks/google.py b/src/any_agent/frameworks/google.py index 1a1c3315..30815b0f 100644 --- a/src/any_agent/frameworks/google.py +++ b/src/any_agent/frameworks/google.py @@ -351,7 +351,7 @@ async def _load_agent(self) -> None: async def _run_async( # type: ignore[no-untyped-def] self, - prompt: str, + prompt: str | list[dict[str, Any]], user_id: str | None = None, session_id: str | None = None, **kwargs, @@ -359,6 +359,9 @@ async def _run_async( # type: ignore[no-untyped-def] if not self._agent: error_message = "Agent not loaded. Call load_agent() first." raise ValueError(error_message) + if not isinstance(prompt, str): + msg = "Google framework does not support list of messages as input. Use a plain string prompt." + raise NotImplementedError(msg) runner = InMemoryRunner(self._agent) user_id = user_id or str(uuid4()) session_id = session_id or str(uuid4()) diff --git a/src/any_agent/frameworks/langchain.py b/src/any_agent/frameworks/langchain.py index 4167aad0..69a431ed 100644 --- a/src/any_agent/frameworks/langchain.py +++ b/src/any_agent/frameworks/langchain.py @@ -288,10 +288,15 @@ async def _load_agent(self) -> None: **agent_args, ) - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: if not self._agent: error_message = "Agent not loaded. Call load_agent() first." raise ValueError(error_message) + if not isinstance(prompt, str): + msg = "LangChain framework does not support list of messages as input. Use a plain string prompt." + raise NotImplementedError(msg) inputs = {"messages": [("user", prompt)]} result = await self._agent.ainvoke(inputs, **kwargs) diff --git a/src/any_agent/frameworks/llama_index.py b/src/any_agent/frameworks/llama_index.py index 144c5063..2ee16be5 100644 --- a/src/any_agent/frameworks/llama_index.py +++ b/src/any_agent/frameworks/llama_index.py @@ -563,10 +563,15 @@ async def _load_agent(self) -> None: **self.config.agent_args or {}, ) - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: if not self._agent: error_message = "Agent not loaded. Call load_agent() first." raise ValueError(error_message) + if not isinstance(prompt, str): + msg = "LlamaIndex framework does not support list of messages as input. Use a plain string prompt." + raise NotImplementedError(msg) result: AgentOutput = await self._agent.run(prompt, **kwargs) # assert that it's a TextBlock if not result.response.blocks or not hasattr(result.response.blocks[0], "text"): diff --git a/src/any_agent/frameworks/openai.py b/src/any_agent/frameworks/openai.py index beab72f8..3fb0a31e 100644 --- a/src/any_agent/frameworks/openai.py +++ b/src/any_agent/frameworks/openai.py @@ -449,10 +449,15 @@ def _filter_mcp_tools(self, tools: list[Any], mcp_clients: list[Any]) -> list[An # The OpenAI framework can handle them as regular tools. return tools - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: if not self._agent: error_message = "Agent not loaded. Call load_agent() first." raise ValueError(error_message) + if not isinstance(prompt, str): + msg = "OpenAI framework does not support list of messages as input. Use a plain string prompt." + raise NotImplementedError(msg) if not kwargs.get("max_turns"): kwargs["max_turns"] = math.inf result = await Runner.run(self._agent, prompt, **kwargs) diff --git a/src/any_agent/frameworks/smolagents.py b/src/any_agent/frameworks/smolagents.py index 8e9cafd0..a32865ec 100644 --- a/src/any_agent/frameworks/smolagents.py +++ b/src/any_agent/frameworks/smolagents.py @@ -362,10 +362,15 @@ async def _load_agent(self) -> None: assert self._agent - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: if not self._agent: error_message = "Agent not loaded. Call load_agent() first." raise ValueError(error_message) + if not isinstance(prompt, str): + msg = "Smolagents framework does not support list of messages as input. Use a plain string prompt." + raise NotImplementedError(msg) result = self._agent.run(prompt, **kwargs) if self.config.output_type: result_json = ( diff --git a/src/any_agent/frameworks/tinyagent.py b/src/any_agent/frameworks/tinyagent.py index a7b255b7..78db4063 100644 --- a/src/any_agent/frameworks/tinyagent.py +++ b/src/any_agent/frameworks/tinyagent.py @@ -171,22 +171,27 @@ async def _load_agent(self) -> None: self.completion_params["tools"].append(function_def) self.clients[tool_name] = ToolExecutor(tool) - async def _run_async(self, prompt: str, **kwargs: Any) -> str | BaseModel: + async def _run_async( + self, prompt: str | list[dict[str, Any]], **kwargs: Any + ) -> str | BaseModel: if self.uses_openai: self.completion_params["tool_choice"] = "auto" if self.config.output_type: self.completion_params["response_format"] = self.config.output_type - messages = [ - { - "role": "system", - "content": self.config.instructions or DEFAULT_SYSTEM_PROMPT, - }, - { - "role": "user", - "content": prompt, - }, - ] + if isinstance(prompt, list): + messages = prompt + else: + messages = [ + { + "role": "system", + "content": self.config.instructions or DEFAULT_SYSTEM_PROMPT, + }, + { + "role": "user", + "content": prompt, + }, + ] if kwargs.pop("max_turns", None): logger.warning( diff --git a/tests/integration/frameworks/test_agent.py b/tests/integration/frameworks/test_agent.py index f7276aa7..735c31ae 100644 --- a/tests/integration/frameworks/test_agent.py +++ b/tests/integration/frameworks/test_agent.py @@ -201,3 +201,20 @@ def write_file(text: str) -> None: html_output = console.export_html(inline_styles=True) with open(f"{trace_path}_trace.html", "w", encoding="utf-8") as f: f.write(html_output.replace("", "")) + + +def test_tinyagent_run_with_messages() -> None: + """Test that TinyAgent.run accepts a list of message dicts instead of a plain string.""" + agent = AnyAgent.create( + AgentFramework.TINYAGENT, + AgentConfig(model_id=DEFAULT_SMALL_MODEL_ID), + ) + + messages = [ + {"role": "system", "content": "You are a helpful assistant. Be concise."}, + {"role": "user", "content": "What is 2 + 2? Reply with just the number."}, + ] + + agent_trace = agent.run(messages) + assert agent_trace.final_output + assert "4" in str(agent_trace.final_output)