Files
agent_alpha/backend/core/agent.py
T

114 lines
4.1 KiB
Python

"""PydanticDeep assistant — deep agentic coding assistant.
PydanticDeep is built on PydanticAI and provides:
- Filesystem tools: ls, read_file, write_file, edit_file, glob, grep
- Task management: write_todos, planning subagent
- Subagent delegation
- Skills system (SKILL.md files)
- Memory persistence (MEMORY.md across sessions)
- Context file discovery (AGENTS.md, SOUL.md from workspace root)
- Built-in web search and web fetch
Backend types (set via PYDANTIC_DEEP_BACKEND_TYPE):
"state" — In-memory (default, no persistence)
"daytona" — Daytona cloud workspace (isolated, cloud-native)
Configuration via settings:
PYDANTIC_DEEP_BACKEND_TYPE : "state" | "daytona" (default: "state")
PYDANTIC_DEEP_INCLUDE_SUBAGENTS : enable subagent delegation (default: True)
PYDANTIC_DEEP_INCLUDE_SKILLS : enable skills system (default: True)
PYDANTIC_DEEP_INCLUDE_PLAN : enable planner subagent (default: True)
PYDANTIC_DEEP_INCLUDE_MEMORY : enable persistent MEMORY.md (default: True)
PYDANTIC_DEEP_INCLUDE_EXECUTE : enable shell execution (default: False)
PYDANTIC_DEEP_WEB_SEARCH : enable built-in web search (default: True)
"""
from __future__ import annotations
import time
from dataclasses import dataclass
from typing import Any
from pydantic_ai import Agent
from backend.repositories.memory_repository import MemoryRepository
from pydantic_deep.deps import DeepAgentDeps
class AgentService:
"""Thin orchestrator for agent execution.
Receives a pre-built ``Agent`` and its ``MemoryRepository`` via
constructor injection so the lifespan (or tests) control wiring.
No lazy initialisation — the agent is ready to run immediately.
"""
def __init__(self, agent: Agent[Any, str], memory_repo: MemoryRepository) -> None:
self._agent = agent
self._memory_repo = memory_repo
# ── Lifecycle ──────────────────────────────────────────────────────────
async def shutdown(self) -> None:
"""Release agent resources."""
self._agent = None
# ── Execution ──────────────────────────────────────────────────────────
async def ask(self, prompt: str, session_id: str | None = None) -> AskResult:
"""Send a prompt to the agent and return the text output with usage stats."""
di = DeepAgentDeps(backend=self._memory_repo.backend)
t0 = time.monotonic()
result = await self._agent.run(prompt, deps=di)
elapsed = time.monotonic() - t0
usage = result.usage
return AskResult(
output=result.output,
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
total_tokens=usage.input_tokens + usage.output_tokens,
elapsed_seconds=round(elapsed, 2),
)
@dataclass
class AskResult:
"""Result of a single agent ``ask()`` call with token usage information."""
output: str
"""The agent's text reply."""
input_tokens: int = 0
"""Number of prompt tokens sent to the model."""
output_tokens: int = 0
"""Number of completion tokens generated by the model."""
total_tokens: int = 0
"""Sum of input + output tokens."""
elapsed_seconds: float = 0.0
"""Wall-clock time in seconds the agent took to respond."""
# ---------------------------------------------------------------------------
# Singleton — wired once at startup by the lifespan hook in ``app.py``.
# Routes access it through ``get_service()``.
# ---------------------------------------------------------------------------
_service: AgentService | None = None
def get_service() -> AgentService:
"""Return the singleton AgentService instance."""
assert _service is not None, "AgentService has not been initialised — did the lifespan run?"
return _service
def set_service(service: AgentService) -> None:
"""Set the module-level singleton (called by the lifespan hook)."""
global _service
_service = service