Files
Heimgeist/backend/agent/ollama_runtime.py

102 lines
4.3 KiB
Python
Raw Normal View History

from __future__ import annotations
import json
from typing import Any, Dict, List
from ..ollama_client import chat_typed
from .registry import NativeToolProvider, ToolExecutionContext
def _usage_dict(usage: Any) -> Dict[str, int]:
return {
"prompt_eval_count": usage.prompt_eval_count,
"eval_count": usage.eval_count,
"total_duration": usage.total_duration,
"load_duration": usage.load_duration,
"prompt_eval_duration": usage.prompt_eval_duration,
"eval_duration": usage.eval_duration,
}
async def run_agent_loop(arguments: Dict[str, Any], context: ToolExecutionContext) -> Dict[str, Any]:
registry = context.registry
if registry is None:
raise RuntimeError("Agent tool registry is unavailable.")
allowed_names = []
for name in arguments.get("allowed_tool_names") or []:
if name == "heimgeist.agent":
continue
registry.get_tool(name)
allowed_names.append(name)
tools = [registry.get_tool(name).ollama_manifest() for name in allowed_names]
messages: List[Dict[str, Any]] = [dict(item) for item in arguments.get("messages") or []]
maximum_rounds = max(1, min(int(arguments.get("maximum_rounds") or 4), 8))
maximum_output_chars = max(1000, min(int(arguments.get("maximum_output_chars") or 60_000), 120_000))
tool_summary: List[Dict[str, Any]] = []
usage_totals: Dict[str, int] = {}
for round_index in range(maximum_rounds):
if context.cancellation_event.is_set():
raise RuntimeError("Agent execution was cancelled.")
if context.consume_llm_call:
context.consume_llm_call()
result = await chat_typed(
arguments["model"],
messages,
options=arguments.get("generation_options") or {},
tools=tools or None,
think=bool(arguments.get("reasoning")),
cancellation_event=context.cancellation_event,
)
for key, value in _usage_dict(result.usage).items():
usage_totals[key] = usage_totals.get(key, 0) + int(value or 0)
if result.thinking and context.show_thinking:
await context.emit("model_thinking", {"text": result.thinking, "round": round_index + 1})
if not result.tool_calls:
content = result.content[:maximum_output_chars]
return {
"content": content,
"tool_summary": tool_summary,
"usage": usage_totals,
"rounds": round_index + 1,
}
assistant_message: Dict[str, Any] = {
"role": "assistant",
"content": result.content or "",
"tool_calls": [call.raw for call in result.tool_calls],
}
messages.append(assistant_message)
for call in result.tool_calls:
if call.name not in allowed_names:
tool_result = {"error": f"Tool is not allowed: {call.name}"}
else:
definition = registry.get_tool(call.name)
approved = True
if definition.requires_confirmation and context.request_confirmation:
approved = await context.request_confirmation(definition, call.arguments)
if approved:
await context.emit("tool_started", {"tool": call.name, "arguments": call.arguments, "agent_round": round_index + 1})
try:
tool_result = await registry.call_tool(call.name, call.arguments, context)
except Exception as exc:
tool_result = {"error": f"{type(exc).__name__}: {exc}"}
await context.emit("tool_result", {"tool": call.name, "result": tool_result, "agent_round": round_index + 1})
else:
tool_result = {"status": "rejected", "confirmed": False}
tool_summary.append({"tool": call.name, "arguments": call.arguments, "result": tool_result})
messages.append({
"role": "tool",
"tool_name": call.name,
"content": json.dumps(tool_result, ensure_ascii=False, default=str),
})
return {
"content": "I could not complete the task within the configured agent round limit.",
"tool_summary": tool_summary,
"usage": usage_totals,
"rounds": maximum_rounds,
}