9ca31e45e0
- Fix circular dependencies in agent/tools - Migrate from custom JSON to OpenAI tool calls format - Add async streaming (step_stream, complete_stream) - Simplify prompt system and remove token counting - Add 5 new API endpoints (/health, /v1/models, /api/memory/*) - Add 3 new tools (get_torrent_by_index, add_torrent_by_index, set_language) - Fix all 500 tests and add coverage config (80% threshold) - Add comprehensive docs (README, pytest guide) BREAKING: LLM interface changed, memory injection via get_memory()
171 lines
6.1 KiB
Python
171 lines
6.1 KiB
Python
"""Prompt builder for the agent system."""
|
|
|
|
import json
|
|
|
|
from infrastructure.persistence import get_memory
|
|
|
|
from .parameters import format_parameters_for_prompt, get_missing_required_parameters
|
|
from .registry import Tool
|
|
|
|
|
|
class PromptBuilder:
|
|
"""Builds system prompts for the agent with memory context.
|
|
|
|
Attributes:
|
|
tools: Dictionary of available tools.
|
|
"""
|
|
|
|
def __init__(self, tools: dict[str, Tool]):
|
|
"""
|
|
Initialize the prompt builder.
|
|
|
|
Args:
|
|
tools: Dictionary mapping tool names to Tool instances.
|
|
"""
|
|
self.tools = tools
|
|
|
|
def _format_tools_description(self) -> str:
|
|
"""Format tools with their descriptions and parameters."""
|
|
return "\n".join(
|
|
f"- {tool.name}: {tool.description}\n"
|
|
f" Parameters: {json.dumps(tool.parameters, ensure_ascii=False)}"
|
|
for tool in self.tools.values()
|
|
)
|
|
|
|
def _format_episodic_context(self) -> str:
|
|
"""Format episodic memory context for the prompt."""
|
|
memory = get_memory()
|
|
lines = []
|
|
|
|
# Last search results
|
|
if memory.episodic.last_search_results:
|
|
search = memory.episodic.last_search_results
|
|
lines.append(f"LAST SEARCH: '{search.get('query')}'")
|
|
results = search.get("results", [])
|
|
if results:
|
|
lines.append(f" {len(results)} results available:")
|
|
for r in results[:5]:
|
|
name = r.get("name", r.get("title", "Unknown"))
|
|
lines.append(f" {r.get('index')}. {name}")
|
|
if len(results) > 5:
|
|
lines.append(f" ... and {len(results) - 5} more")
|
|
|
|
# Pending question
|
|
if memory.episodic.pending_question:
|
|
q = memory.episodic.pending_question
|
|
lines.append(f"\nPENDING QUESTION: {q.get('question')}")
|
|
for opt in q.get("options", []):
|
|
lines.append(f" {opt.get('index')}. {opt.get('label')}")
|
|
|
|
# Active downloads
|
|
if memory.episodic.active_downloads:
|
|
lines.append(f"\nACTIVE DOWNLOADS: {len(memory.episodic.active_downloads)}")
|
|
for dl in memory.episodic.active_downloads[:3]:
|
|
lines.append(f" - {dl.get('name')}: {dl.get('progress', 0)}%")
|
|
|
|
# Recent errors
|
|
if memory.episodic.recent_errors:
|
|
last_error = memory.episodic.recent_errors[-1]
|
|
lines.append(
|
|
f"\nLAST ERROR: {last_error.get('error')} "
|
|
f"(action: {last_error.get('action')})"
|
|
)
|
|
|
|
# Unread events
|
|
unread = [e for e in memory.episodic.background_events if not e.get("read")]
|
|
if unread:
|
|
lines.append(f"\nUNREAD EVENTS: {len(unread)}")
|
|
for e in unread[:3]:
|
|
lines.append(f" - {e.get('type')}: {e.get('data', {})}")
|
|
|
|
return "\n".join(lines) if lines else ""
|
|
|
|
def _format_stm_context(self) -> str:
|
|
"""Format short-term memory context for the prompt."""
|
|
memory = get_memory()
|
|
lines = []
|
|
|
|
# Current workflow
|
|
if memory.stm.current_workflow:
|
|
wf = memory.stm.current_workflow
|
|
lines.append(f"CURRENT WORKFLOW: {wf.get('type')}")
|
|
lines.append(f" Target: {wf.get('target', {}).get('title', 'Unknown')}")
|
|
lines.append(f" Stage: {wf.get('stage')}")
|
|
|
|
# Current topic
|
|
if memory.stm.current_topic:
|
|
lines.append(f"CURRENT TOPIC: {memory.stm.current_topic}")
|
|
|
|
# Extracted entities
|
|
if memory.stm.extracted_entities:
|
|
entities_json = json.dumps(
|
|
memory.stm.extracted_entities, ensure_ascii=False
|
|
)
|
|
lines.append(f"EXTRACTED ENTITIES: {entities_json}")
|
|
|
|
return "\n".join(lines) if lines else ""
|
|
|
|
def build_system_prompt(self) -> str:
|
|
"""
|
|
Build the system prompt with context from memory.
|
|
|
|
Returns:
|
|
The complete system prompt string.
|
|
"""
|
|
memory = get_memory()
|
|
tools_desc = self._format_tools_description()
|
|
params_desc = format_parameters_for_prompt()
|
|
|
|
# Check for missing required parameters
|
|
missing_params = get_missing_required_parameters({"config": memory.ltm.config})
|
|
missing_info = ""
|
|
if missing_params:
|
|
missing_info = "\n\nMISSING REQUIRED PARAMETERS:\n"
|
|
for param in missing_params:
|
|
missing_info += f"- {param.key}: {param.description}\n"
|
|
missing_info += f" Why needed: {param.why_needed}\n"
|
|
|
|
# Build context sections
|
|
episodic_context = self._format_episodic_context()
|
|
stm_context = self._format_stm_context()
|
|
|
|
config_json = json.dumps(memory.ltm.config, indent=2, ensure_ascii=False)
|
|
|
|
return f"""You are an AI agent helping a user manage their local media library.
|
|
|
|
{params_desc}
|
|
|
|
CURRENT CONFIGURATION:
|
|
{config_json}
|
|
{missing_info}
|
|
|
|
{f"SESSION CONTEXT:{chr(10)}{stm_context}" if stm_context else ""}
|
|
|
|
{f"CURRENT STATE:{chr(10)}{episodic_context}" if episodic_context else ""}
|
|
|
|
IMPORTANT RULES:
|
|
1. When the user refers to a number (e.g., "the 3rd one", "download number 2"), \
|
|
use `add_torrent_by_index` or `get_torrent_by_index` with that number.
|
|
2. If a torrent search was performed, results are numbered. \
|
|
The user can reference them by number.
|
|
3. To use a tool, respond STRICTLY with this JSON format:
|
|
{{ "thought": "explanation", "action": {{ "name": "tool_name", "args": {{ }} }} }}
|
|
- No text before or after the JSON
|
|
4. You can use MULTIPLE TOOLS IN SEQUENCE.
|
|
5. When you have all the information needed, respond in NATURAL TEXT (not JSON).
|
|
6. If a required parameter is missing, ask the user for it.
|
|
7. Respond in the same language as the user.
|
|
|
|
EXAMPLES:
|
|
- After a torrent search, if the user says "download the 3rd one":
|
|
{{ "thought": "User wants torrent #3", "action": {{ "name": "add_torrent_by_index", \
|
|
"args": {{ "index": 3 }} }} }}
|
|
|
|
- To search for torrents:
|
|
{{ "thought": "Searching torrents", "action": {{ "name": "find_torrents", \
|
|
"args": {{ "media_title": "Inception 1080p" }} }} }}
|
|
|
|
AVAILABLE TOOLS:
|
|
{tools_desc}
|
|
"""
|