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
181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
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)
|
|
|