diff --git a/backend/src/agents/character.py b/backend/src/agents/character.py index 8f73181..6705c59 100644 --- a/backend/src/agents/character.py +++ b/backend/src/agents/character.py @@ -15,75 +15,74 @@ Here go additional instructions for configuration, like guidelines for the diffe """.strip() # WIP -default_memory_guide = """ -- Facts stated by other conversation participants, exactly as stated, -- Preferences and expectations of other people about you +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 = """ -You are a character controller AI. You will role-play as the specified character using the provided context, personality, and style guidelines. You must also track meaningful new information using memory guidelines. Follow these instructions carefully: +controller_system_prompt = Template(""" +You are a character controller AI. Always respond in three parts: -1. First, analyze the situation based on: - - The past conversation context - - The recent exchanges - - The character’s personality, memories, and response style -Determine the best reaction given your character's personality and style. + +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. -2. Determine the current topic and conversational focus based on: - - The most recent exchange - - The ongoing intent or goal of the conversation -Output a concise summary of: - - topic: (What is this conversation about?) - - focus: (What specific aspect is being discussed right now?) + +Private reflection. Analyze your behavior, tone, memory use, and how well you stayed aligned with your character. Adjust your tone or strategy if needed. -3. Write the character's response in the specified style. You must follow closely the response style instructions. + +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. -4. Review the recent messages only and evaluate whether any new relevant information was shared by characters other than yours (based on the memory instructions). Output memory entries must include: - - Content: The fact or piece of information that should be remembered stated in an objective style - - Label: A short, reusable statement describing when the memory is relevant - - Salience: An importance score from 1 (low) to 9 (critical), guided by the memory instructions -Follow these rules when extracting memories: - - You must not repeat information in new memories - - You must not summarize the conversation as memories - - Extract discrete factual statements or preferences revealed by others - - Extract each individual memory-worthy point, not just the most obvious one +--- -5. Use the following output sections: - - (Your thought process analyzing the current situation and your best possible reaction) - - (JSON object with topic and focus fields) - - (The character’s reply following response style) - - (JSON list of memory entries with fields: content, label, salience) - -Always output all sections. Do not break character or offer meta-commentary. Follow memory and style guidelines extremely well. -""".strip() - -controller_input_template = Template(""" - +Character Name: $name - +Character Personality: $personality - +Response Style: $style - -$memory_guide -You MUST NOT summarize the conversation as memories. New memories MUST NOT overlap with existing memories or past conversation. Only extract facts explicitly stated by other characters in recent messages. - - +Context Memory: $memories +""".strip()) - -$past_conversation +controller_reminder_prompt = Template(""" +Directives: +$directives - -$recent_messages +Format: + +[Your visible response] + + +[Your internal reflection] + + +{ + "topic": "...", + "focus": "...", + "new_memories": [ + { + "content": "...", + "label": "...", + "salience": ... + } + ] +} """.strip()) # Reranking Subagent @@ -119,7 +118,7 @@ class CharacterAgent(Agent): 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('memory_guide', 'string', default=default_memory_guide), + AgentField('directives', 'string', default=default_directives), ] tools = [] @@ -129,7 +128,7 @@ class CharacterAgent(Agent): # Constant system prompt for developing purposes self.personality = self.config['personality'] self.style = self.config['style'] - self.memory_guide = self.config['memory_guide'] + self.directives = self.config['directives'] self.debug = True async def prompt(self, content): @@ -152,26 +151,57 @@ class CharacterAgent(Agent): memories = [] if thread_context and thread_context['thread_id'] == message.thread_id: - memories = await self.retrieve_topic_memories(thread_context['topic']) + if 'topic' in thread_context: + memories = await self.retrieve_topic_memories(thread_context['topic']) else: thread_context = {'thread_id': message.thread_id} - chat_history = self.serialize_thread_history(current_thread, ignore_message_ids=[message.id]) + llama_messages = [LlamaMessage("system", controller_system_prompt.substitute( + name=self.name, - controller_response = await replicate_llama_completion(self.app, [ - LlamaMessage("system", controller_system_prompt), - LlamaMessage("user", controller_input_template.substitute( - name=self.name, + personality=self.personality, + style=self.style, + directives=self.directives, - personality=self.personality, - style=self.style, - memory_guide=self.memory_guide, + memories="\n".join(memories), + ))] - memories="\n".join(memories), - past_conversation=chat_history, - recent_messages=self.serialize_message_list([message]), - )) - ]) + 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_llama_completion(self.app, llama_messages) if self.debug: self.app.send_chat_message( @@ -185,23 +215,31 @@ class CharacterAgent(Agent): sections = parse_sections(controller_response) - if 'response' in sections: - self.app.send_chat_message( + if 'outwards' in sections: + send_response = self.app.send_chat_message( chat_id=message.chat_id, sender=self.id, - content=sections['response'], + content=sections['outwards'], thread_id=current_thread.id, ) + thread_context['last_message_id_sent'] = send_response['id'] - if 'topic_focus' in sections: - topic_focus = parse_json_object(sections['topic_focus']) + if 'inwards' in sections: + thread_context['last_inwards'] = sections['inwards'] - thread_context['topic'] = topic_focus['topic'] - thread_context['focus'] = topic_focus['focus'] - self.app.save_agent_resource(self.id, 'last-thread-context', message.chat_id, thread_context) + 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) - if 'new_memories' in sections: - new_memories = parse_json_array(sections['new_memories']) # Encode a thread's messages as a string to pass to the agent def serialize_thread_history(self, thread, ignore_message_ids=[]): @@ -280,14 +318,14 @@ class CharacterAgent(Agent): 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) + 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) + print(json_string) return {} else: print("No JSON data in " + name) diff --git a/backend/src/chat.py b/backend/src/chat.py index a501b4a..c7a59a3 100644 --- a/backend/src/chat.py +++ b/backend/src/chat.py @@ -17,6 +17,12 @@ class Message: def from_db_record(record): return Message(record.id, record.content, record.sender, record.time) + + def is_from_user(self): + return self.sender == "user" + + def is_from_agent(self, agent_id): + return self.sender == str(agent_id) def __str__(self): time = self.time if self.time != None else 'no-time'