chore: release api v0.3.4
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Task Agent implementation using PydanticAI.
|
||||
|
||||
Full orchestrator agent that can:
|
||||
- Execute multi-step tasks autonomously
|
||||
- Use all tools (read + write)
|
||||
- Spawn sub-agents (Explore, Plan) for focused work
|
||||
"""
|
||||
import os
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
|
||||
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
|
||||
from src.domains.agents.task.prompts import TASK_SYSTEM_PROMPT
|
||||
from src.ollama.provider import get_ollama_provider
|
||||
from src.shared.config import get_settings
|
||||
from src.shared.logging import logged, get_logger, trace_span
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskContext(AgentContext):
|
||||
"""
|
||||
Context for task agent tools.
|
||||
|
||||
Passed to all tool functions via RunContext.
|
||||
Uses the same fields as base AgentContext.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TaskAgentImpl(BaseAgent):
|
||||
"""
|
||||
Full orchestrator agent for autonomous task execution.
|
||||
|
||||
Has access to ALL tools:
|
||||
- Read-only: read_file, glob_files, grep_content, bash_readonly
|
||||
- Write: edit_file, write_file, bash
|
||||
- External: web_search
|
||||
- Orchestration: spawn_agent (launch sub-agents)
|
||||
|
||||
Can spawn Explore and Plan agents to offload focused tasks,
|
||||
keeping context efficient across complex multi-step work.
|
||||
"""
|
||||
|
||||
name = "task"
|
||||
description = "Autonomous multi-step task execution with sub-agent orchestration"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the task agent."""
|
||||
self._agent: Agent[TaskContext, str] | None = None
|
||||
self._settings = get_settings()
|
||||
|
||||
def _create_agent(self) -> Agent[TaskContext, str]:
|
||||
"""Create the PydanticAI agent with Ollama backend."""
|
||||
# Use sanitized Ollama provider to fix content: null issues
|
||||
model = OpenAIModel(
|
||||
model_name=self._settings.ollama_agent_model,
|
||||
provider=get_ollama_provider(),
|
||||
)
|
||||
|
||||
agent: Agent[TaskContext, str] = Agent(
|
||||
model=model,
|
||||
system_prompt=TASK_SYSTEM_PROMPT,
|
||||
deps_type=TaskContext,
|
||||
output_type=str,
|
||||
# Mistral Nemo settings:
|
||||
# - temperature 0.3 (Nemo needs slightly higher than 0.0)
|
||||
# - tool_choice "required" forces tool use
|
||||
model_settings={
|
||||
"temperature": 0.3,
|
||||
"extra_body": {"tool_choice": "required"},
|
||||
},
|
||||
)
|
||||
|
||||
# Register all tools including orchestration
|
||||
self._register_tools(agent)
|
||||
|
||||
return agent
|
||||
|
||||
def _register_tools(self, agent: Agent[TaskContext, str]) -> None:
|
||||
"""Register all tools with the agent."""
|
||||
from src.domains.agents.task.tools import register_task_tools
|
||||
register_task_tools(agent)
|
||||
|
||||
@logged()
|
||||
async def run(
|
||||
self,
|
||||
prompt: str,
|
||||
working_dir: str | None = None,
|
||||
allowed_paths: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
) -> str:
|
||||
"""
|
||||
Run the task agent to execute a multi-step task.
|
||||
|
||||
Args:
|
||||
prompt: Description of the task to execute
|
||||
working_dir: Working directory for the agent
|
||||
allowed_paths: Restrict tool access to these paths
|
||||
|
||||
Returns:
|
||||
Consolidated task summary with results
|
||||
"""
|
||||
ctx = TaskContext(
|
||||
working_dir=working_dir or os.getcwd(),
|
||||
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
|
||||
timeout_seconds=self._settings.tool_timeout_seconds,
|
||||
)
|
||||
|
||||
async with trace_span("task_agent_run"):
|
||||
try:
|
||||
# Use run() not run_stream() - Ollama has bugs with streaming + tools
|
||||
result = await self.agent.run(prompt, deps=ctx)
|
||||
return result.output
|
||||
except Exception as e:
|
||||
logger.exception(f"Task agent error: {e}")
|
||||
raise
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
prompt: str,
|
||||
working_dir: str | None = None,
|
||||
allowed_paths: list[str] | None = None,
|
||||
**kwargs: Any
|
||||
) -> AsyncIterator[str]:
|
||||
"""
|
||||
Run the task agent with streaming output.
|
||||
|
||||
Yields text chunks as they become available.
|
||||
"""
|
||||
ctx = TaskContext(
|
||||
working_dir=working_dir or os.getcwd(),
|
||||
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
|
||||
timeout_seconds=self._settings.tool_timeout_seconds,
|
||||
)
|
||||
|
||||
async with trace_span("task_agent_stream"):
|
||||
try:
|
||||
async with self.agent.run_stream(prompt, deps=ctx) as result:
|
||||
async for chunk in result.stream_text():
|
||||
yield chunk
|
||||
except Exception as e:
|
||||
logger.exception(f"Task agent stream error: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# Create and register the singleton instance
|
||||
task_agent = TaskAgentImpl()
|
||||
register_agent(task_agent)
|
||||
|
||||
|
||||
async def task(
|
||||
prompt: str,
|
||||
working_dir: str | None = None,
|
||||
**kwargs: Any
|
||||
) -> str:
|
||||
"""Run task execution."""
|
||||
return await task_agent.run(prompt, working_dir=working_dir, **kwargs)
|
||||
|
||||
|
||||
async def task_stream(
|
||||
prompt: str,
|
||||
working_dir: str | None = None,
|
||||
**kwargs: Any
|
||||
) -> AsyncIterator[str]:
|
||||
"""Run task execution with streaming."""
|
||||
async for chunk in task_agent.run_stream(prompt, working_dir=working_dir, **kwargs):
|
||||
yield chunk
|
||||
Reference in New Issue
Block a user