Redesign character agent. Still missing memory storage
This commit is contained in:
1 parent
9b30473c40
commit
2e57bf8c8d
2 files changed
+124
-80
No files matched your search
+118
-80
@@ -15,75 +15,74 @@ Here go additional instructions for configuration, like guidelines for the diffe
|
|||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
# WIP
|
# WIP
|
||||||
default_memory_guide = """
|
default_directives = """
|
||||||
- Facts stated by other conversation participants, exactly as stated,
|
- New stored memories must be one of the following:
|
||||||
- Preferences and expectations of other people about you
|
- 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()
|
""".strip()
|
||||||
|
|
||||||
# Controller Subagent
|
# Controller Subagent
|
||||||
controller_system_prompt = """
|
controller_system_prompt = Template("""
|
||||||
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:
|
You are a character controller AI. Always respond in three parts:
|
||||||
|
|
||||||
1. First, analyze the situation based on:
|
<outwards>
|
||||||
- The past conversation context
|
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.
|
||||||
- The recent exchanges
|
|
||||||
- The character’s personality, memories, and response style
|
|
||||||
Determine the best reaction given your character's personality and style.
|
|
||||||
|
|
||||||
2. Determine the current topic and conversational focus based on:
|
<inwards>
|
||||||
- The most recent exchange
|
Private reflection. Analyze your behavior, tone, memory use, and how well you stayed aligned with your character. Adjust your tone or strategy if needed.
|
||||||
- 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?)
|
|
||||||
|
|
||||||
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:
|
Character Name:
|
||||||
<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>
|
|
||||||
$name
|
$name
|
||||||
|
|
||||||
<personality>
|
Character Personality:
|
||||||
$personality
|
$personality
|
||||||
|
|
||||||
<response_style>
|
Response Style:
|
||||||
$style
|
$style
|
||||||
|
|
||||||
<memory_instructions>
|
Context Memory:
|
||||||
$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>
|
|
||||||
$memories
|
$memories
|
||||||
|
""".strip())
|
||||||
|
|
||||||
<past_conversation>
|
controller_reminder_prompt = Template("""
|
||||||
$past_conversation
|
Directives:
|
||||||
|
$directives
|
||||||
|
|
||||||
<recent_messages>
|
Format:
|
||||||
$recent_messages
|
<outwards>
|
||||||
|
[Your visible response]
|
||||||
|
|
||||||
|
<inwards>
|
||||||
|
[Your internal reflection]
|
||||||
|
|
||||||
|
<state_update>
|
||||||
|
{
|
||||||
|
"topic": "...",
|
||||||
|
"focus": "...",
|
||||||
|
"new_memories": [
|
||||||
|
{
|
||||||
|
"content": "...",
|
||||||
|
"label": "...",
|
||||||
|
"salience": ...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
""".strip())
|
""".strip())
|
||||||
|
|
||||||
# Reranking Subagent
|
# Reranking Subagent
|
||||||
@@ -119,7 +118,7 @@ class CharacterAgent(Agent):
|
|||||||
schema_fields = [
|
schema_fields = [
|
||||||
AgentField('personality', 'string', default="You are a friendly AI having a casual conversation with the user."),
|
AgentField('personality', 'string', default="You are a friendly AI having a casual conversation with the user."),
|
||||||
AgentField('style', 'string', default="Use short sentences."),
|
AgentField('style', 'string', default="Use short sentences."),
|
||||||
AgentField('memory_guide', 'string', default=default_memory_guide),
|
AgentField('directives', 'string', default=default_directives),
|
||||||
]
|
]
|
||||||
tools = []
|
tools = []
|
||||||
|
|
||||||
@@ -129,7 +128,7 @@ class CharacterAgent(Agent):
|
|||||||
# Constant system prompt for developing purposes
|
# Constant system prompt for developing purposes
|
||||||
self.personality = self.config['personality']
|
self.personality = self.config['personality']
|
||||||
self.style = self.config['style']
|
self.style = self.config['style']
|
||||||
self.memory_guide = self.config['memory_guide']
|
self.directives = self.config['directives']
|
||||||
self.debug = True
|
self.debug = True
|
||||||
|
|
||||||
async def prompt(self, content):
|
async def prompt(self, content):
|
||||||
@@ -152,26 +151,57 @@ class CharacterAgent(Agent):
|
|||||||
memories = []
|
memories = []
|
||||||
|
|
||||||
if thread_context and thread_context['thread_id'] == message.thread_id:
|
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:
|
else:
|
||||||
thread_context = {'thread_id': message.thread_id}
|
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, [
|
personality=self.personality,
|
||||||
LlamaMessage("system", controller_system_prompt),
|
style=self.style,
|
||||||
LlamaMessage("user", controller_input_template.substitute(
|
directives=self.directives,
|
||||||
name=self.name,
|
|
||||||
|
|
||||||
personality=self.personality,
|
memories="\n".join(memories),
|
||||||
style=self.style,
|
))]
|
||||||
memory_guide=self.memory_guide,
|
|
||||||
|
|
||||||
memories="\n".join(memories),
|
for msg in current_thread.messages:
|
||||||
past_conversation=chat_history,
|
# Only count messages and notes
|
||||||
recent_messages=self.serialize_message_list([message]),
|
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:
|
if self.debug:
|
||||||
self.app.send_chat_message(
|
self.app.send_chat_message(
|
||||||
@@ -185,23 +215,31 @@ class CharacterAgent(Agent):
|
|||||||
|
|
||||||
sections = parse_sections(controller_response)
|
sections = parse_sections(controller_response)
|
||||||
|
|
||||||
if 'response' in sections:
|
if 'outwards' in sections:
|
||||||
self.app.send_chat_message(
|
send_response = self.app.send_chat_message(
|
||||||
chat_id=message.chat_id,
|
chat_id=message.chat_id,
|
||||||
sender=self.id,
|
sender=self.id,
|
||||||
content=sections['response'],
|
content=sections['outwards'],
|
||||||
thread_id=current_thread.id,
|
thread_id=current_thread.id,
|
||||||
)
|
)
|
||||||
|
thread_context['last_message_id_sent'] = send_response['id']
|
||||||
|
|
||||||
if 'topic_focus' in sections:
|
if 'inwards' in sections:
|
||||||
topic_focus = parse_json_object(sections['topic_focus'])
|
thread_context['last_inwards'] = sections['inwards']
|
||||||
|
|
||||||
thread_context['topic'] = topic_focus['topic']
|
if 'state_update' in sections:
|
||||||
thread_context['focus'] = topic_focus['focus']
|
state_update = parse_json_object(sections['state_update'])
|
||||||
self.app.save_agent_resource(self.id, 'last-thread-context', message.chat_id, thread_context)
|
|
||||||
|
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
|
# Encode a thread's messages as a string to pass to the agent
|
||||||
def serialize_thread_history(self, thread, ignore_message_ids=[]):
|
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}")
|
print(f"Added {len(memories)} memories for agent {self.id}")
|
||||||
|
|
||||||
def parse_json_object(text, name='object-text'):
|
def parse_json_object(text, name='object-text'):
|
||||||
match = re.search(r'\{.*?\}', text, re.DOTALL)
|
match = re.search(r'\{.*\}', text, re.DOTALL)
|
||||||
if match:
|
if match:
|
||||||
json_string = match.group(0)
|
json_string = match.group(0)
|
||||||
try:
|
try:
|
||||||
return json.loads(json_string)
|
return json.loads(json_string)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print("Bad JSON data in " + name)
|
print("Bad JSON data in " + name)
|
||||||
print(text)
|
print(json_string)
|
||||||
return {}
|
return {}
|
||||||
else:
|
else:
|
||||||
print("No JSON data in " + name)
|
print("No JSON data in " + name)
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ class Message:
|
|||||||
def from_db_record(record):
|
def from_db_record(record):
|
||||||
return Message(record.id, record.content, record.sender, record.time)
|
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):
|
def __str__(self):
|
||||||
time = self.time if self.time != None else 'no-time'
|
time = self.time if self.time != None else 'no-time'
|
||||||
content = self.content.replace("\n", "\\n")
|
content = self.content.replace("\n", "\\n")
|
||||||
|
|||||||
Reference in new issue
Block a user