Redesign character agent. Still missing memory storage

This commit is contained in:
Shivering Ocelot committed 2025-07-29 11:50:12 -05:00
1 parent 9b30473c40
commit 2e57bf8c8d
2 files changed
+124 -80

No files matched your search

+118 -80
View File
@@ -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.
<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.
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?)
<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.
3. Write the character's response in the specified style. You must follow closely the response style instructions.
<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.
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:
<analysis>
(Your thought process analyzing the current situation and your best possible reaction)
<topic_focus>
(JSON object with topic and focus fields)
<response>
(The character’s reply following response style)
<new_memories>
(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("""
<name>
Character Name:
$name
<personality>
Character Personality:
$personality
<response_style>
Response Style:
$style
<memory_instructions>
$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.
<existing_memories>
Context Memory:
$memories
""".strip())
<past_conversation>
$past_conversation
controller_reminder_prompt = Template("""
Directives:
$directives
<recent_messages>
$recent_messages
Format:
<outwards>
[Your visible response]
<inwards>
[Your internal reflection]
<state_update>
{
"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 = "<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_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)
+6
View File
@@ -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'