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

+27 -86
View File
@@ -1,10 +1,4 @@
import re
import json
import inspect
from string import Template
import inference
from inference import LlamaMessage
from agent import Agent, AgentField
CLASS_DESCRIPTION = """
@@ -27,90 +21,37 @@ class ExpertAgent(Agent):
def __init__(self, app, data):
Agent.__init__(self, app, data)
async def prompt(self, instruction, input=None, **kwargs):
# Instruction extends system if there's input or messages
# Otherwise instruction is the user prompt
# Input is always added as a user message at the end, if present
# Use 'messages' as starting point when passed
messages = []
if 'messages' in kwargs:
messages = [LlamaMessage(msg['role'], msg['content']) for msg in kwargs['messages']]
# Construct the default messages
system = self.config['system_prompt']
prompt = input
if input == None and not messages:
prompt = instruction
elif instruction:
system += "\n" + instruction
# Add default messages to list
messages.insert(0, LlamaMessage("system", system))
if prompt:
messages.append(LlamaMessage("user", prompt))
model_id = self.config['model']
db_model = await self.app.get_model(model_id)
model_engine = db_model.value["engine"]
async def run(self, run):
inp = run.input or {}
opts = {}
if 'temperature' in self.config:
opts['temperature'] = self.config['temperature']
if 'repetition_penalty' in self.config:
opts['repetition_penalty'] = self.config['repetition_penalty']
if model_engine == "llama-cpp":
response = await inference.local_chat_completion(
self.app,
messages,
model=db_model.value['model'],
opts=opts,
)
elif model_engine == "replicate":
if 'parameters' in db_model:
opts['parameters'] = db_model['parameters']
if 'format' in db_model:
opts['format'] = db_model['format']
response = await inference.replicate_completion(self.app, messages, model=db_model.value['model'], opts=opts)
if self.config.get("temperature") is not None:
opts["temperature"] = self.config["temperature"]
if self.config.get("repetition_penalty") is not None:
opts["repetition_penalty"] = self.config["repetition_penalty"]
elif model_engine == "ollama":
response = await inference.ollama_chat_completion(self.app, messages, model=db_model.value['model'], opts=opts)
# 1. Build message list based on input mode
if "messages" in inp:
messages = inp["messages"]
else:
raise Exception("Unsuported model: " + repr(db_model.value))
instruction = inp.get("instruction")
input_text = inp.get("input")
messages = inference.build_messages(
system_prompt=self.config.get("system_prompt", ""),
instruction=instruction,
input=input_text,
messages=inp.get("messages"),
)
return response
async def received_message(self, message):
chat_view = await self.app.get_chat_view(message.chat_id)
thread = chat_view.get_thread(message.thread_id)
def msg_role(message):
sender = message.sender
is_digit = isinstance(sender, (int, str)) and str(sender).isdigit()
if is_digit and str(sender) == str(self.id):
return 'assistant'
elif sender == 'user':
return 'user'
elif sender.startswith("tmp-"):
return 'assistant'
else:
raise Exception("Unsupported sender: " + sender)
messages = [{'role': msg_role(msg), 'content': msg.content} for msg in thread.messages]
response = await self.prompt(
instruction=None,
input=None,
messages=messages
# 2. Call inference, passing the run so it feeds run.stream
# and can emit tool_call events through the run.
await inference.chat_completion(
self.app,
model_id=self.config.get("model"),
messages=messages,
tools=inp.get("tools", []),
opts=opts,
agent_run=run,
)
await self.app.send_chat_message(
chat_id=message.chat_id,
sender=self.id,
content=response,
thread_id=message.thread_id,
)
run.stream.listen(lambda chunk: None, lambda: run.close())