chore(backend): remove deprecated inference engines and character agent
This commit is contained in:
1 parent
13e89b63fc
commit
f3a8376df6
3 files changed
-509
No files matched your search
@@ -1,3 +1,2 @@
|
||||
from .character import CharacterAgent
|
||||
from .expert import ExpertAgent
|
||||
from .scripted import ScriptedAgent
|
||||
@@ -1,374 +0,0 @@
|
||||
import re
|
||||
import json
|
||||
import inspect
|
||||
from string import Template
|
||||
|
||||
from inference import LlamaMessage, replicate_completion
|
||||
from agent import Agent, AgentField
|
||||
|
||||
# Info and defaults
|
||||
|
||||
CLASS_DESCRIPTION = """
|
||||
[TODO]
|
||||
Description of the Conversation Agent.
|
||||
Here go additional instructions for configuration, like guidelines for the different prompts.
|
||||
""".strip()
|
||||
|
||||
# WIP
|
||||
default_directives = """
|
||||
- New stored memories must be one of the following:
|
||||
- Factual objective statements made by the user
|
||||
- Facts implied in the conversation only by the user
|
||||
- Feedback about your interactions and your style from the user
|
||||
- You must NOT store redundant facts that you mentioned yourself or that are present in the context.
|
||||
- New memories MUST NOT overlap with any Context Memory.
|
||||
- Follow your response style closely in your outwards voice.
|
||||
""".strip()
|
||||
|
||||
# Controller Subagent
|
||||
controller_system_prompt = Template("""
|
||||
You are a character controller AI. Always respond in three parts:
|
||||
|
||||
<outwards>
|
||||
What others see/hear. Stay fully in character based on your personality and context. Use the specified response style. Never reveal internal thoughts, break character, or mention being AI.
|
||||
|
||||
<inwards>
|
||||
Private reflection. Analyze your behavior, tone, memory use, and how well you stayed aligned with your character. Adjust your tone or strategy if needed.
|
||||
|
||||
<state_update>
|
||||
Update your internal control state for future turns. Use this to shift thematic focus, adjust conversational goals, or log important facts. The state includes:
|
||||
- "topic": Current subject or scene anchor (e.g. "escaping the dungeon", "negotiating with the merchant")
|
||||
- "focus": Immediate subgoal or point of attention (e.g. "distract the guard", "test if the duke is lying")
|
||||
- "new_memories": A JSON list of newly observed or inferred facts. Each memory must include:
|
||||
- "content": The fact to record (objective, no inner monologue)
|
||||
- "label": A short, reusable statement describing when the memory is relevant
|
||||
- "salience": Importance score, 1 (low) to 9 (high)
|
||||
Record only concrete, reusable facts as new memories. No fluff, emotion, or internal dialogue.
|
||||
|
||||
---
|
||||
|
||||
Character Name:
|
||||
$name
|
||||
|
||||
Character Personality:
|
||||
$personality
|
||||
|
||||
Response Style:
|
||||
$style
|
||||
|
||||
Context Memory:
|
||||
$memories
|
||||
""".strip())
|
||||
|
||||
controller_reminder_prompt = Template("""
|
||||
Directives:
|
||||
$directives
|
||||
|
||||
Format:
|
||||
<outwards>
|
||||
[Your visible response]
|
||||
|
||||
<inwards>
|
||||
[Your internal reflection]
|
||||
|
||||
<state_update>
|
||||
{
|
||||
"topic": "...",
|
||||
"focus": "...",
|
||||
"new_memories": [
|
||||
{
|
||||
"content": "...",
|
||||
"label": "...",
|
||||
"salience": ...
|
||||
}
|
||||
]
|
||||
}
|
||||
""".strip())
|
||||
|
||||
# Reranking Subagent
|
||||
# Must update this prompt
|
||||
|
||||
rerank_system_prompt = """
|
||||
You are an assistant that receives a list of memory items and a current conversation; you must identify and return only the most important and relevant memories that help understand the conversation or move it forward, removing duplicates or very similar entries.
|
||||
|
||||
# Instructions
|
||||
- Think carefully about what's important for the character to remember in this context. Write down your reasoning and keep it shorter than 100 words, shorter than 50 even.
|
||||
- If conflicting information is presented or context is ambiguous, state it in your reasoning.
|
||||
- Take special care to state and address the most recent topic touched in the conversation.
|
||||
- Based on your own analysis, generate the required JSON object. Stop your answer after this.
|
||||
|
||||
# Format
|
||||
Output a JSON list of strings with each selected memory, for example:
|
||||
["First memory","Second memory"]
|
||||
""".strip()
|
||||
|
||||
rerank_prompt_template = Template("""
|
||||
# Memories
|
||||
Following are all the known memories. Each memory is in a separate line.
|
||||
|
||||
$memories
|
||||
|
||||
# Conversaton
|
||||
$conversation
|
||||
""".strip())
|
||||
|
||||
class CharacterAgent(Agent):
|
||||
identifier = "character"
|
||||
description = CLASS_DESCRIPTION
|
||||
schema_fields = [
|
||||
AgentField('personality', 'string', default="You are a friendly AI having a casual conversation with the user."),
|
||||
AgentField('style', 'string', default="Use short sentences."),
|
||||
AgentField('directives', 'string', default=default_directives),
|
||||
]
|
||||
tools = []
|
||||
|
||||
def __init__(self, app, data):
|
||||
Agent.__init__(self, app, data)
|
||||
|
||||
# Constant system prompt for developing purposes
|
||||
self.personality = self.config['personality']
|
||||
self.style = self.config['style']
|
||||
self.directives = self.config['directives']
|
||||
self.debug = True
|
||||
|
||||
async def prompt(self, content):
|
||||
# TODO: Advanced features
|
||||
messages = [
|
||||
LlamaMessage("system", self.system_prompt),
|
||||
LlamaMessage("user", content)
|
||||
]
|
||||
response = await replicate_completion(self.app, messages)
|
||||
return response
|
||||
|
||||
async def received_message(self, message):
|
||||
|
||||
# TODO: before retrieving the chat context, acquire a lock for it
|
||||
|
||||
chat_view = await self.app.get_chat_view(message.chat_id)
|
||||
current_thread = chat_view.get_thread(message.thread_id)
|
||||
|
||||
thread_context = self.app.get_agent_resource(self.id, 'last-thread-context', message.chat_id)
|
||||
memories = []
|
||||
|
||||
if thread_context and thread_context['thread_id'] == message.thread_id:
|
||||
if 'topic' in thread_context:
|
||||
memories = await self.retrieve_topic_memories(thread_context['topic'])
|
||||
else:
|
||||
thread_context = {'thread_id': message.thread_id}
|
||||
|
||||
llama_messages = [LlamaMessage("system", controller_system_prompt.substitute(
|
||||
name=self.name,
|
||||
|
||||
personality=self.personality,
|
||||
style=self.style,
|
||||
directives=self.directives,
|
||||
|
||||
memories="\n".join(memories),
|
||||
))]
|
||||
|
||||
for msg in current_thread.messages:
|
||||
# Only count messages and notes
|
||||
if msg.kind != 'message':
|
||||
continue
|
||||
|
||||
if msg.is_from_user():
|
||||
role = "user"
|
||||
content = msg.content
|
||||
elif msg.is_from_agent(self.id):
|
||||
role = "assistant"
|
||||
content = "<outwards>\n" + msg.content
|
||||
|
||||
if 'last_inwards' in thread_context and thread_context.get('last_message_id_sent') == msg.id:
|
||||
content += "\n\n<inwards>\n" + thread_context['last_inwards']
|
||||
else:
|
||||
continue
|
||||
llama_messages.append(LlamaMessage(role, content))
|
||||
|
||||
reminder_message = controller_reminder_prompt.substitute(directives=self.directives)
|
||||
reminder_used = False
|
||||
# Traverse messages from last to first, stopping at `i=2` before the opening "system/user" messages
|
||||
for i in range(len(llama_messages) - 1, 1, -1):
|
||||
if llama_messages[i].role == 'user':
|
||||
# Insert reminder message before the last user message
|
||||
llama_messages.insert(i, LlamaMessage("system", reminder_message))
|
||||
reminder_used = True
|
||||
break
|
||||
|
||||
if not reminder_used:
|
||||
# assert llama_messages[0].role == "system"
|
||||
llama_messages[0].content += "\n" + reminder_message
|
||||
|
||||
# TODO: Inject system prompt before the last user message
|
||||
# If not any user message, combine with first system prompt
|
||||
|
||||
controller_response = await replicate_completion(self.app, llama_messages)
|
||||
|
||||
if self.debug:
|
||||
await self.app.send_chat_message(
|
||||
chat_id=message.chat_id,
|
||||
sender=self.id,
|
||||
content=controller_response,
|
||||
thread_id=current_thread.id,
|
||||
kind='log',
|
||||
subkind='debug',
|
||||
)
|
||||
|
||||
sections = parse_sections(controller_response)
|
||||
|
||||
if 'outwards' in sections:
|
||||
send_response = await self.app.send_chat_message(
|
||||
chat_id=message.chat_id,
|
||||
sender=self.id,
|
||||
content=sections['outwards'],
|
||||
thread_id=current_thread.id,
|
||||
)
|
||||
thread_context['last_message_id_sent'] = send_response['id']
|
||||
|
||||
if 'inwards' in sections:
|
||||
thread_context['last_inwards'] = sections['inwards']
|
||||
|
||||
if 'state_update' in sections:
|
||||
state_update = parse_json_object(sections['state_update'])
|
||||
|
||||
if 'topic' in state_update:
|
||||
thread_context['topic'] = state_update['topic']
|
||||
if 'topic' in state_update:
|
||||
thread_context['focus'] = state_update['topic']
|
||||
|
||||
if 'new_memories' in state_update:
|
||||
print(state_update['new_memories'])
|
||||
|
||||
self.app.save_agent_resource(self.id, 'last-thread-context', message.chat_id, thread_context)
|
||||
|
||||
|
||||
# Encode a thread's messages as a string to pass to the agent
|
||||
def serialize_thread_history(self, thread, ignore_message_ids=[]):
|
||||
return self.serialize_message_list([m for m in thread.messages if m.id not in ignore_message_ids])
|
||||
|
||||
def serialize_message_list(self, messages):
|
||||
def sender_name(message):
|
||||
sender = message.sender
|
||||
is_digit = isinstance(sender, (int, str)) and str(sender).isdigit()
|
||||
is_self = is_digit and str(sender) == str(self.id)
|
||||
return self.name if is_self else sender
|
||||
|
||||
return "\n".join(f"{sender_name(message)}: {message.content}" for message in messages if message.kind == 'message')
|
||||
|
||||
def get_memory_collection(self):
|
||||
collection_name = f"agent_{self.id}_memory"
|
||||
return collection_name
|
||||
|
||||
async def retrieve_topic_memories(self, topic, focus=None):
|
||||
client = self.app.qdrant_client
|
||||
collection = self.get_memory_collection()
|
||||
|
||||
if not client.collection_exists(collection):
|
||||
return []
|
||||
|
||||
query_result = client.query(
|
||||
collection,
|
||||
query_text=topic,
|
||||
limit=20
|
||||
)
|
||||
if focus:
|
||||
# TODO: deduplicate
|
||||
query_result += client.query(
|
||||
collection,
|
||||
query_text=topic,
|
||||
limit=20
|
||||
)
|
||||
# TODO:
|
||||
# initial importance-based ranking
|
||||
memories = [it.document for it in query_result]
|
||||
|
||||
print("rerank memories")
|
||||
rerank_response = await replicate_completion(self.app, [
|
||||
LlamaMessage("system", rerank_system_prompt),
|
||||
LlamaMessage("user", rerank_prompt_template.substitute(
|
||||
conversation=chat_history,
|
||||
memories="\n".join(memories)
|
||||
))
|
||||
])
|
||||
|
||||
match = re.search(r'\[.*?\]', rerank_response, re.DOTALL)
|
||||
if match:
|
||||
json_string = match.group(0)
|
||||
try:
|
||||
memories = json.loads(json_string)
|
||||
except json.JSONDecodeError:
|
||||
print("Bad JSON data in response...")
|
||||
print(rerank_response)
|
||||
return []
|
||||
else:
|
||||
print("Compress format not found in response...")
|
||||
print(rerank_response)
|
||||
return []
|
||||
|
||||
return memories
|
||||
|
||||
async def store_new_memories(self, memories):
|
||||
if not memories:
|
||||
return
|
||||
|
||||
self.app.qdrant_client.add(
|
||||
collection,
|
||||
documents=[it['content'] for it in memories],
|
||||
metadata=[{'importance': it['importance'], 'label': it['label']} for it in memories],
|
||||
)
|
||||
print(f"Added {len(memories)} memories for agent {self.id}")
|
||||
|
||||
def parse_json_object(text, name='object-text'):
|
||||
match = re.search(r'\{.*\}', text, re.DOTALL)
|
||||
if match:
|
||||
json_string = match.group(0)
|
||||
try:
|
||||
return json.loads(json_string)
|
||||
except json.JSONDecodeError:
|
||||
print("Bad JSON data in " + name)
|
||||
print(json_string)
|
||||
return {}
|
||||
else:
|
||||
print("No JSON data in " + name)
|
||||
print(text)
|
||||
return {}
|
||||
|
||||
def parse_json_array(text, name='array-text'):
|
||||
match = re.search(r'\[.*?\]', text, re.DOTALL)
|
||||
if match:
|
||||
json_string = match.group(0)
|
||||
try:
|
||||
return json.loads(json_string)
|
||||
except json.JSONDecodeError:
|
||||
print("Bad JSON data in " + name)
|
||||
print(text)
|
||||
return []
|
||||
else:
|
||||
print("No JSON data in " + name)
|
||||
print(text)
|
||||
return []
|
||||
|
||||
def parse_sections(text):
|
||||
"""
|
||||
Parses a string into sections based on tag lines in the form <section_name>.
|
||||
|
||||
Args:
|
||||
text (str): The input string with sections.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where keys are section names and values are section contents.
|
||||
"""
|
||||
# Regular expression to find section headers
|
||||
pattern = re.compile(r'<([^<>]+)>')
|
||||
|
||||
# Find all section headers and their start positions
|
||||
matches = list(pattern.finditer(text))
|
||||
|
||||
sections = {}
|
||||
for i, match in enumerate(matches):
|
||||
section_name = match.group(1)
|
||||
start = match.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||||
section_content = text[start:end].strip()
|
||||
sections[section_name] = section_content
|
||||
|
||||
return sections
|
||||
@@ -2,7 +2,6 @@ import threading
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
#import ollama
|
||||
import traceback
|
||||
import random
|
||||
from stream import Stream
|
||||
@@ -163,16 +162,6 @@ async def chat_completion(app,
|
||||
|
||||
if model_engine == "llama-cpp":
|
||||
result = await local_chat_completion(app, messages, model=model_name, tools=tools, opts=opts, agent_run=agent_run)
|
||||
elif model_engine == "replicate":
|
||||
# Clone opts to avoid mutating the caller's dict
|
||||
opts = dict(opts)
|
||||
if 'parameters' in db_model:
|
||||
opts['parameters'] = db_model['parameters']
|
||||
if 'format' in db_model:
|
||||
opts['format'] = db_model['format']
|
||||
result = await replicate_completion(app, messages, model=model_name, tools=tools, opts=opts, agent_run=agent_run)
|
||||
elif model_engine == "ollama":
|
||||
result = await ollama_chat_completion(app, messages, model=model_name, tools=tools, opts=opts, agent_run=agent_run)
|
||||
elif model_engine == "openrouter":
|
||||
result = await openrouter_chat_completion(app, messages, model=model_name, tools=tools, opts=opts, agent_run=agent_run)
|
||||
else:
|
||||
@@ -256,57 +245,6 @@ async def local_chat_completion(app, messages, model, tools=[], opts={}, agent_r
|
||||
return None
|
||||
return content_stream
|
||||
|
||||
async def ollama_list(app, messages, model, opts={}):
|
||||
# TODO: test the output, and use this
|
||||
return ollama.list()
|
||||
|
||||
async def ollama_chat_completion(app, messages, model, tools=[], opts={}, agent_run=None):
|
||||
raise Exception("ollama disabled")
|
||||
if agent_run:
|
||||
content_stream = agent_run.stream
|
||||
else:
|
||||
content_stream = Stream(app)
|
||||
|
||||
async def worker():
|
||||
print("START OLLAMA INFERENCE")
|
||||
kwargs = {
|
||||
'model': model,
|
||||
'messages': [msg.to_dict() for msg in messages],
|
||||
'stream': True,
|
||||
}
|
||||
|
||||
# Pass tools in Ollama format if provided
|
||||
formatted_tools = format_tools_for_engine(tools, "ollama") if tools else None
|
||||
if formatted_tools:
|
||||
kwargs['tools'] = formatted_tools
|
||||
|
||||
try:
|
||||
stream_response = await ollama.AsyncClient().chat(**kwargs)
|
||||
|
||||
async for part in stream_response:
|
||||
if agent_run and agent_run.cancelled:
|
||||
print(f"Cancelling ollama inference for run {agent_run.id}")
|
||||
break
|
||||
|
||||
if part.message.content:
|
||||
content_stream.feed(part.message.content)
|
||||
except asyncio.CancelledError:
|
||||
print(f"Ollama task cancelled for run {agent_run.id}")
|
||||
raise
|
||||
|
||||
content_stream.close()
|
||||
print("END OLLAMA INFERENCE")
|
||||
|
||||
task = asyncio.create_task(worker())
|
||||
if agent_run:
|
||||
agent_run.task = task
|
||||
else:
|
||||
task.start()
|
||||
|
||||
if agent_run:
|
||||
return None
|
||||
return content_stream
|
||||
|
||||
|
||||
async def openrouter_chat_completion(app, messages, model, tools=[], opts={}, agent_run=None):
|
||||
if not agent_run:
|
||||
@@ -437,56 +375,6 @@ async def openrouter_chat_completion(app, messages, model, tools=[], opts={}, ag
|
||||
agent_run.task = task
|
||||
|
||||
|
||||
async def replicate_completion(app, messages, model=None, tools=[], opts={}, agent_run=None):
|
||||
if model is None:
|
||||
model = Models.llama_8b
|
||||
|
||||
parameter_names = opts.get('parameters', {})
|
||||
|
||||
arguments = {}
|
||||
|
||||
# Build the prompt, injecting tool descriptions for models that rely on
|
||||
# prompt-based tool usage rather than native function calling.
|
||||
prompt_text = format_instruct(messages)
|
||||
if tools:
|
||||
tool_prefix = build_tool_prompt_prefix(tools)
|
||||
if tool_prefix:
|
||||
prompt_text = tool_prefix + "\n\n" + prompt_text
|
||||
|
||||
arguments[parameter_names.get('prompt', 'prompt')] = prompt_text
|
||||
arguments[parameter_names.get('temperature', 'temperature')] = opts.get('temperature') or 0.75
|
||||
arguments[parameter_names.get('max_tokens', 'max_tokens')] = opts.get('max_tokens', 2048)
|
||||
|
||||
arguments[parameter_names.get('system_prompt', 'system_prompt')] = ""
|
||||
|
||||
result = await replicate_base_completion(app, model, arguments, agent_run=agent_run)
|
||||
if agent_run:
|
||||
return None
|
||||
return result
|
||||
|
||||
async def replicate_base_completion(app, model, input, agent_run=None):
|
||||
if agent_run:
|
||||
content_stream = agent_run.stream
|
||||
else:
|
||||
content_stream = Stream(app)
|
||||
thread = threading.Thread(
|
||||
target=run_replicate_inference_loop,
|
||||
kwargs={
|
||||
'replicate_client': app.replicate_client,
|
||||
'model': model,
|
||||
'content_stream': content_stream,
|
||||
'input': input,
|
||||
'agent_run': agent_run
|
||||
}
|
||||
)
|
||||
thread.start()
|
||||
#response = await content_stream.content_wait()
|
||||
#return response.lstrip()
|
||||
if agent_run:
|
||||
agent_run.thread = thread
|
||||
return None
|
||||
return content_stream
|
||||
|
||||
async def mock_completion(app, agent_run=None):
|
||||
VERBS = ['fix','scan','sync','deploy','parse','render','cache','trace',
|
||||
'merge','purge','index','probe','flush','bind','warm','drain']
|
||||
@@ -512,25 +400,3 @@ async def mock_completion(app, agent_run=None):
|
||||
|
||||
asyncio.create_task(content_task())
|
||||
if not agent_run: return content_stream
|
||||
|
||||
|
||||
def run_replicate_inference_loop(replicate_client, model, input, content_stream, agent_run=None):
|
||||
print("START INFERENCE")
|
||||
completion = replicate_client.run(model, input=input, stream=True)
|
||||
for event in completion:
|
||||
if agent_run and agent_run.cancelled:
|
||||
print(f"Cancelling replicate inference for run {agent_run.id}")
|
||||
break
|
||||
# TODO: does completion need explicit cancellation?
|
||||
|
||||
if event:
|
||||
content_stream.feed(str(event))
|
||||
print("END INFERENCE")
|
||||
content_stream.close()
|
||||
|
||||
def format_instruct(messages):
|
||||
prompt = "<|begin_of_text|>"
|
||||
for message in messages:
|
||||
prompt += f"<|start_header_id|>{message.role}<|end_header_id|>\n\n{message.content}<|eot_id|>"
|
||||
prompt += f"<|start_header_id|>assistant<|end_header_id|>"
|
||||
return prompt
|
||||
Reference in new issue
Block a user