Refactor agent execution to use AgentRun with OpenAI-compatible API

Major refactoring to support isolated run objects with streaming, events, and OpenAI-compatible endpoints.

## Backend Changes

- **Agent class**: Added `run()` method, `received_message()` wraps chat into AgentRun
- **AgentRun class**: New class for run state with event system, serialization, and user tracking
- **inference.py**: Complete refactor with `build_messages()`, `chat_completion()`, tool support for llama-cpp/ollama/replicate
- **tools.py**: New centralized tool management (ToolSpec, ToolParameter, format_tools_for_engine)
- **tools.py**: Removed legacy Toolset/Action/ToolDescription system
- **server.py**: Added OpenAI-compatible `/api/openai/v1/chat/completions` endpoint with streaming support
- **temporary.py**: Added `self.runs` dict to State class
- **expert.py**: Refactored to use new Agent.run() signature
- **scripted.py**: Updated to raise NotImplementedError

## Frontend Changes

- **backend.js**: Added `onRunEvents()` for run event subscriptions
- **Sidebar.vue**: Added runs navigation mode
- **router/index.js**: Renamed User route to RunsView
- **RunsView.vue**: New view for listing/monitoring agent runs

## Breaking Changes

- Agent `prompt()` signature changed: `(instruction, input=None, **kwargs)` instead of `(content)`
- Agent `received_message()` must use AgentRun pattern
- Tools.py removed, functionality moved to tool.py
- Deprecated tool prompt/action system removed
This commit is contained in:
Shivering Ocelot committed 2026-05-25 18:05:17 -05:00
1 parent eb9a1ad9e8
commit 4e15b44d39
14 files changed
+1151 -522

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
class ToolParameter:
"""Describes a single input parameter for a tool.
Mirrors the JSON-schema style used by OpenAI / llama-cpp function calling.
Attributes:
name: Parameter name.
type: JSON-schema type string, e.g. "string", "number", "boolean".
description: Human-readable description of the parameter.
required: Whether the parameter is required.
enum: Optional list of allowed values.
"""
def __init__(self, name, type="string", description="", required=True, enum=None):
self.name = name
self.type = type
self.description = description
self.required = required
self.enum = enum
def to_dict(self):
d = {
"type": self.type,
"description": self.description,
}
if self.enum is not None:
d["enum"] = self.enum
return d
def __repr__(self):
return f"ToolParameter(name={self.name!r}, type={self.type!r})"
class ToolSpec:
"""Declarative specification for a tool callable by an LLM.
A ToolSpec describes *what* a tool does and *what* parameters it accepts.
It is used at inference time to build the tool-description payload sent to
the model so the model can emit structured tool-call requests.
Inspired by ``langchain.tools.StructuredTool`` and OpenAI's
``tools`` / ``functions`` request format.
Attributes:
name: Unique identifier for the tool.
description: Short human-readable description shown to the model.
parameters: List of ``ToolParameter`` objects.
# execution
func: Optional callable that executes the tool. When present the
orchestrator can dispatch a ``ToolCall`` automatically.
"""
def __init__(self, name, description="", parameters=None, func=None):
self.name = name
self.description = description
self.parameters = parameters or []
self.func = func
# -- serialization helpers -------------------------------------------
def to_openai_dict(self):
"""Return a dict in OpenAI chat-completions ``tools`` format."""
properties = {}
required = []
for p in self.parameters:
properties[p.name] = p.to_dict()
if p.required:
required.append(p.name)
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
},
},
}
def to_replicate_dict(self):
"""Return a dict suitable for Replicate model input.
Replicate models do not have a standard tool-calling protocol, so we
return a simplified representation that can be injected into the
prompt as text.
"""
param_lines = []
for p in self.parameters:
req = " (required)" if p.required else " (optional)"
param_lines.append(f" - {p.name} ({p.type}){req}: {p.description}")
params_text = "\n".join(param_lines) if param_lines else " (none)"
return {
"name": self.name,
"description": self.description,
"parameters_text": f"{self.name}:\n{params_text}",
}
def execute(self, **kwargs):
"""Synchronously run the tool with the given keyword arguments."""
if self.func is None:
raise RuntimeError(f"ToolSpec '{self.name}' has no callable (func is None)")
return self.func(**kwargs)
async def execute_async(self, **kwargs):
"""Asynchronously run the tool with the given keyword arguments."""
if self.func is None:
raise RuntimeError(f"ToolSpec '{self.name}' has no callable (func is None)")
if asyncio.iscoroutinefunction(self.func):
return await self.func(**kwargs)
return self.func(**kwargs)
def __repr__(self):
return f"ToolSpec(name={self.name!r}, params={[p.name for p in self.parameters]})"
class ToolCall:
"""Represents a single tool invocation request emitted by the model.
Attributes:
id: Unique call identifier (used by OpenAI / llama-cpp protocol).
tool_name: Name of the tool to invoke.
arguments: Dict of argument name → value.
result: String content returned by the tool.
is_error: Whether the result represents an error.
"""
def __init__(self, tool_name, arguments=None, id=None):
self.id = id or ""
self.tool_name = tool_name
self.arguments = arguments or {}
self.result = None
def __repr__(self):
return f"ToolCall(tool={self.tool_name!r}, args={self.arguments!r})"
def call_message(self):
raise Exception("TODO: Llama message for tool call")
def result_message(self):
from inference import LlamaMessage
return LlamaMessage("tool", self.result)
# ---------------------------------------------------------------------------
# Helpers to format tool specs per engine
# ---------------------------------------------------------------------------
def format_tools_for_engine(tools, engine):
"""Convert a list of ``ToolSpec`` objects for the given engine.
Returns a value suitable to pass as the ``tools`` kwarg to the underlying
chat-completion call.
"""
specs = [t for t in tools]
if engine in ("llama-cpp", "ollama"):
return [s.to_openai_dict() for s in specs]
elif engine == "replicate":
return [s.to_replicate_dict() for s in specs]
else:
return [s.to_openai_dict() for s in specs]
def build_tool_prompt_prefix(tools):
"""Build a text prefix describing available tools for engines that rely
on prompt-injection (e.g. Replicate) rather than native tool-calling."""
if not tools:
return ""
parts = ["You have access to the following tools:"]
for t in tools:
spec = t
info = spec.to_replicate_dict()
parts.append(info["parameters_text"])
parts.append("")
return "\n".join(parts)