From f3a8376df6fc89df87a088a89b2cf69991556baf Mon Sep 17 00:00:00 2001 From: Shivering Ocelot Date: Mon, 28 Sep 2026 15:00:54 -0500 Subject: [PATCH] chore(backend): remove deprecated inference engines and character agent --- backend/src/agents/__init__.py | 1 - backend/src/agents/character.py | 374 -------------------------------- backend/src/inference.py | 134 ------------ 3 files changed, 509 deletions(-) delete mode 100644 backend/src/agents/character.py diff --git a/backend/src/agents/__init__.py b/backend/src/agents/__init__.py index dc17f72..a5f3205 100644 --- a/backend/src/agents/__init__.py +++ b/backend/src/agents/__init__.py @@ -1,3 +1,2 @@ -from .character import CharacterAgent from .expert import ExpertAgent from .scripted import ScriptedAgent \ No newline at end of file diff --git a/backend/src/agents/character.py b/backend/src/agents/character.py deleted file mode 100644 index 0ed967b..0000000 --- a/backend/src/agents/character.py +++ /dev/null @@ -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: - - -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. - - -Private reflection. Analyze your behavior, tone, memory use, and how well you stayed aligned with your character. Adjust your tone or strategy if needed. - - -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: - -[Your visible response] - - -[Your internal reflection] - - -{ - "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 = "\n" + msg.content - - if 'last_inwards' in thread_context and thread_context.get('last_message_id_sent') == msg.id: - content += "\n\n\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 . - - 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 \ No newline at end of file diff --git a/backend/src/inference.py b/backend/src/inference.py index acd39e4..3c26800 100644 --- a/backend/src/inference.py +++ b/backend/src/inference.py @@ -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