separate and improve conversation agent
This commit is contained in:
1 parent
951ceaa05b
commit
3c3febb92d
6 files changed
+161
-109
No files matched your search
@@ -75,92 +75,6 @@ class DebugAgent(Agent):
|
|||||||
thread_id=chat_view.current_thread_id,
|
thread_id=chat_view.current_thread_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ConversationAgent(Agent):
|
|
||||||
identifier = "conversation"
|
|
||||||
schema_fields = []
|
|
||||||
tools = []
|
|
||||||
|
|
||||||
def __init__(self, app, data):
|
|
||||||
Agent.__init__(self, app, data)
|
|
||||||
|
|
||||||
# Constant system prompt for developing purposes
|
|
||||||
self.system_prompt = "You are a friendly AI having a casual conversation with the user."
|
|
||||||
|
|
||||||
async def prompt(self, content):
|
|
||||||
# TODO: Advanced features
|
|
||||||
messages = [
|
|
||||||
LlamaMessage("system", self.system_prompt),
|
|
||||||
LlamaMessage("user", content)
|
|
||||||
]
|
|
||||||
response = await replicate_llama_completion(self.app, messages)
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def received_message(self, message):
|
|
||||||
|
|
||||||
# TODO: before retrieving the chat context, acquire a lock for it
|
|
||||||
|
|
||||||
saved_context = ThreadContext(self.app.get_agent_resource(self.id, 'last-thread-context', message.chat_id))
|
|
||||||
chat_view = self.app.get_chat_view(message.chat_id)
|
|
||||||
print("ChatView:\n" + chat_view.debug_string())
|
|
||||||
|
|
||||||
if message.thread_id != saved_context.current_thread_id:
|
|
||||||
# the incoming message is not from the same thread that the agent was last aware of
|
|
||||||
# store details about the old thread in the agent's long term memory
|
|
||||||
old_thread = chat_view.get_thread(saved_context.current_thread_id)
|
|
||||||
await self.process_thread(old_thread)
|
|
||||||
|
|
||||||
# TODO: This is a good place to store the new context, its fine that the agent hasnt responded
|
|
||||||
|
|
||||||
current_thread = chat_view.get_thread(message.thread_id)
|
|
||||||
|
|
||||||
# REMOVE THIS. Only for testing, while the function only prints...
|
|
||||||
await self.process_thread(current_thread)
|
|
||||||
|
|
||||||
chat_summary = self.serialize_thread_history(current_thread)
|
|
||||||
messages = [
|
|
||||||
LlamaMessage("system", f"{self.system_prompt}\nYou are given the context of the conversation, respond with your next message in the conversation."),
|
|
||||||
LlamaMessage("user", f"Conversation History:\n{chat_summary}")
|
|
||||||
]
|
|
||||||
|
|
||||||
response = await replicate_llama_completion(self.app, messages)
|
|
||||||
self.app.send_chat_message(
|
|
||||||
chat_id=message.chat_id,
|
|
||||||
sender=self.id,
|
|
||||||
content=response,
|
|
||||||
thread_id=current_thread.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: drop the chat context lock
|
|
||||||
|
|
||||||
# Encode a thread's messages as a string to pass to the agent
|
|
||||||
def serialize_thread_history(self, thread):
|
|
||||||
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 thread.messages)
|
|
||||||
|
|
||||||
# Compute thread summnary and concrete memories, then save the progress done
|
|
||||||
async def process_thread(self, thread):
|
|
||||||
# TODO: extract memories from the thread messages, then store them in the agent's vector memory db
|
|
||||||
# TODO: somewhere here it must be known if the thread was seen before: the same memory should not be stored multiple times
|
|
||||||
print(f"PROCESSING THREAD: {thread.id}")
|
|
||||||
|
|
||||||
instruction = "Your task is to identify facts from a conversation that its participants would remember in the long term. Each line in your response is a different memory, if there's nothing relevant to remember, simply give an empty response."
|
|
||||||
intro = f"Identify the facts that {self.name} will remember long term after the conversation. Following the line break is the personality prompt for {self.name}."
|
|
||||||
|
|
||||||
chat_history = self.serialize_thread_history(thread)
|
|
||||||
messages = [
|
|
||||||
LlamaMessage("system", f"{instruction}\n{intro}\n\n{self.system_prompt}"),
|
|
||||||
LlamaMessage("user", f"Conversation History:\n{chat_history}")
|
|
||||||
]
|
|
||||||
response = await replicate_llama_completion(self.app, messages)
|
|
||||||
memories = [line for line in response.split('\n') if line.strip()]
|
|
||||||
|
|
||||||
print("New agent memories:", memories)
|
|
||||||
|
|
||||||
class ToolUsingAgent:
|
class ToolUsingAgent:
|
||||||
identifier = "tool-using"
|
identifier = "tool-using"
|
||||||
schema_fields = [
|
schema_fields = [
|
||||||
@@ -353,14 +267,6 @@ class CoderV1Agent(Llama8bAgent):
|
|||||||
async def gral_update_file(self, message_history: list, filepath: str, file_content: str):
|
async def gral_update_file(self, message_history: list, filepath: str, file_content: str):
|
||||||
return await gral.gral_update_file(self.app, self.config['system_prompt'], message_history, filepath, file_content)
|
return await gral.gral_update_file(self.app, self.config['system_prompt'], message_history, filepath, file_content)
|
||||||
|
|
||||||
# Used by ConversationAgent to keep track of the last chat thread it participated in
|
|
||||||
class ThreadContext:
|
|
||||||
def __init__(self, data):
|
|
||||||
if data:
|
|
||||||
self.current_thread_id = data.get('current_thread_id')
|
|
||||||
else:
|
|
||||||
self.current_thread_id = None
|
|
||||||
|
|
||||||
class ParsedResponse:
|
class ParsedResponse:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.response = None
|
self.response = None
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import re
|
||||||
|
import json
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from inference import LlamaMessage, replicate_llama_completion
|
||||||
|
from agent import Agent
|
||||||
|
|
||||||
|
memory_system_prompt = """
|
||||||
|
You are an expert in simulating human-like memory for fictional characters. Given a conversation between two characters and a detailed description of one character's personality, extract the memories that this character is most likely to retain.
|
||||||
|
The extracted memories should reflect:
|
||||||
|
- What was emotionally significant to them
|
||||||
|
- What aligns with their goals, fears, or desires
|
||||||
|
- What they found surprising, confusing, or meaningful
|
||||||
|
- What they might revisit or ruminate on later
|
||||||
|
Do not summarize the conversation. Instead, highlight key moments or phrases that the character would mentally hold onto, framed from their perspective. Avoid information stated by yourself, you don't need to remember what you already know, instead you can identify the other's characters reactions to what your character says if it's notable or salient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
memory_prompt_template = """
|
||||||
|
# Your Character
|
||||||
|
Name: [name]
|
||||||
|
Personality and Role: [personality]
|
||||||
|
|
||||||
|
# Output Format
|
||||||
|
Respond only with a JSON array, where each object represents a memory, and each memory object has the following fields:
|
||||||
|
- memory: Short description of the fact that the character will remember, phrased objectively and in third person.
|
||||||
|
- importance: Salience score of the memory, where 5 is for facts that are definitely important for either of the characters or that may be referenced frequently, and 1 indicates facts or events of low importance or short term relevance.
|
||||||
|
If there are no relevant memories to extract, it is ok to respond simply with an empty list.
|
||||||
|
|
||||||
|
# Conversation History
|
||||||
|
[conversation]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Used to keep track of the last chat thread it participated in
|
||||||
|
class ThreadContext:
|
||||||
|
def __init__(self, data):
|
||||||
|
if data:
|
||||||
|
self.current_thread_id = data.get('current_thread_id')
|
||||||
|
else:
|
||||||
|
self.current_thread_id = None
|
||||||
|
|
||||||
|
class ConversationAgent(Agent):
|
||||||
|
identifier = "conversation"
|
||||||
|
schema_fields = []
|
||||||
|
tools = []
|
||||||
|
|
||||||
|
def __init__(self, app, data):
|
||||||
|
Agent.__init__(self, app, data)
|
||||||
|
|
||||||
|
# Constant system prompt for developing purposes
|
||||||
|
self.system_prompt = "You are a friendly AI having a casual conversation with the user."
|
||||||
|
|
||||||
|
async def prompt(self, content):
|
||||||
|
# TODO: Advanced features
|
||||||
|
messages = [
|
||||||
|
LlamaMessage("system", self.system_prompt),
|
||||||
|
LlamaMessage("user", content)
|
||||||
|
]
|
||||||
|
response = await replicate_llama_completion(self.app, messages)
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def received_message(self, message):
|
||||||
|
|
||||||
|
# TODO: before retrieving the chat context, acquire a lock for it
|
||||||
|
|
||||||
|
saved_context = ThreadContext(self.app.get_agent_resource(self.id, 'last-thread-context', message.chat_id))
|
||||||
|
chat_view = self.app.get_chat_view(message.chat_id)
|
||||||
|
print("ChatView:\n" + chat_view.debug_string())
|
||||||
|
|
||||||
|
if message.thread_id != saved_context.current_thread_id:
|
||||||
|
# the incoming message is not from the same thread that the agent was last aware of
|
||||||
|
# store details about the old thread in the agent's long term memory
|
||||||
|
old_thread = chat_view.get_thread(saved_context.current_thread_id)
|
||||||
|
await self.process_thread(old_thread)
|
||||||
|
|
||||||
|
# TODO: This is a good place to store the new context, its fine that the agent hasnt responded
|
||||||
|
|
||||||
|
current_thread = chat_view.get_thread(message.thread_id)
|
||||||
|
|
||||||
|
# REMOVE THIS. Only for testing, while the function only prints...
|
||||||
|
await self.process_thread(current_thread)
|
||||||
|
|
||||||
|
chat_summary = self.serialize_thread_history(current_thread)
|
||||||
|
messages = [
|
||||||
|
LlamaMessage("system", f"{self.system_prompt}\nYou are given the context of the conversation, respond with your next message in the conversation."),
|
||||||
|
LlamaMessage("user", f"Conversation History:\n{chat_summary}")
|
||||||
|
]
|
||||||
|
|
||||||
|
response = await replicate_llama_completion(self.app, messages)
|
||||||
|
self.app.send_chat_message(
|
||||||
|
chat_id=message.chat_id,
|
||||||
|
sender=self.id,
|
||||||
|
content=response,
|
||||||
|
thread_id=current_thread.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: drop the chat context lock
|
||||||
|
|
||||||
|
# Encode a thread's messages as a string to pass to the agent
|
||||||
|
def serialize_thread_history(self, thread):
|
||||||
|
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 thread.messages)
|
||||||
|
|
||||||
|
# Compute thread summnary and concrete memories, then save the progress done
|
||||||
|
async def process_thread(self, thread):
|
||||||
|
# TODO: extract memories from the thread messages, then store them in the agent's vector memory db
|
||||||
|
# TODO: somewhere here it must be known if the thread was seen before: the same memory should not be stored multiple times
|
||||||
|
print(f"PROCESSING THREAD: {thread.id}")
|
||||||
|
|
||||||
|
chat_history = self.serialize_thread_history(thread)
|
||||||
|
prompt = memory_prompt_template.replace("[name]", self.name).replace("[personality]", self.system_prompt).replace("[conversation]", chat_history)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
LlamaMessage("system", memory_system_prompt),
|
||||||
|
LlamaMessage("user", prompt)
|
||||||
|
]
|
||||||
|
response = await replicate_llama_completion(self.app, messages)
|
||||||
|
|
||||||
|
match = re.search(r'\[.*?\]', response, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
json_string = match.group(0)
|
||||||
|
try:
|
||||||
|
memories = json.loads(json_string)
|
||||||
|
print("Extracted Memories:", memories)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("Bad JSON data in response...")
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Memory format not found in response...")
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
memories = [line for line in response.split('\n') if line.strip()]
|
||||||
|
|
||||||
|
print("New agent memories:", memories)
|
||||||
+3
-1
@@ -15,6 +15,8 @@ import agent as agent_mod
|
|||||||
from server import start_server
|
from server import start_server
|
||||||
from chat import ChatView, Message
|
from chat import ChatView, Message
|
||||||
|
|
||||||
|
from agents.conversation import ConversationAgent
|
||||||
|
|
||||||
MESSAGE_PAGE_SIZE = 32
|
MESSAGE_PAGE_SIZE = 32
|
||||||
|
|
||||||
class ChatContext:
|
class ChatContext:
|
||||||
@@ -41,7 +43,7 @@ class App:
|
|||||||
# `Agent` is the kind 'basic', will be class `BasicAgent` in the future
|
# `Agent` is the kind 'basic', will be class `BasicAgent` in the future
|
||||||
self.agent_classes = [
|
self.agent_classes = [
|
||||||
agent_mod.BasicAgent,
|
agent_mod.BasicAgent,
|
||||||
agent_mod.ConversationAgent,
|
ConversationAgent,
|
||||||
agent_mod.Llama8bAgent,
|
agent_mod.Llama8bAgent,
|
||||||
agent_mod.CoderV1Agent,
|
agent_mod.CoderV1Agent,
|
||||||
agent_mod.ToolUsingAgent,
|
agent_mod.ToolUsingAgent,
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_title = chatInfo.name;
|
_title = chatInfo.name;
|
||||||
|
|
||||||
currentThreadId = initialMessages.last.threadId;
|
currentThreadId = initialMessages.last.threadId;
|
||||||
if (currentThreadId != null) {
|
if (currentThreadId == null) {
|
||||||
|
createThread();
|
||||||
|
} else {
|
||||||
expandedThreads.add(currentThreadId!);
|
expandedThreads.add(currentThreadId!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -151,9 +153,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendMessage(String content) {
|
void sendMessage(String content) async {
|
||||||
if (content.trim().isEmpty) return; // Don't send empty messages
|
if (content.trim().isEmpty) return; // Don't send empty messages
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
|
|
||||||
final newMessage = Message(sender: "user", content: content, threadId: currentThreadId);
|
final newMessage = Message(sender: "user", content: content, threadId: currentThreadId);
|
||||||
Backend.sendAction("send-chat-message", {
|
Backend.sendAction("send-chat-message", {
|
||||||
'chatId': widget.chatId,
|
'chatId': widget.chatId,
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.13.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -53,10 +53,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -164,10 +164,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "10.0.9"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -361,10 +361,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.3.1"
|
version: "15.0.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ source .venv/bin/activate
|
|||||||
if [ -f .env ]; then
|
if [ -f .env ]; then
|
||||||
source .env
|
source .env
|
||||||
export REPLICATE_API_TOKEN
|
export REPLICATE_API_TOKEN
|
||||||
|
else
|
||||||
|
echo "No .env file found."
|
||||||
|
echo "Write a .env file with the variables: REPLICATE_API_TOKEN"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
"")
|
"")
|
||||||
mkdir -p local-data
|
data_dir=$(realpath ./local-data)
|
||||||
|
mkdir -p $data_dir
|
||||||
cd backend
|
cd backend
|
||||||
python app.py ../test-data
|
python app.py $data_dir
|
||||||
;;
|
;;
|
||||||
"cli")
|
"cli")
|
||||||
node cli/main.js "$2"
|
node cli/main.js "$2"
|
||||||
@@ -60,8 +64,7 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
if [ -d "$1" ]; then
|
if [ -d "$1" ]; then
|
||||||
data_dir=../$1
|
data_dir=$(realpath $1)
|
||||||
mkdir -p local-data
|
|
||||||
cd backend
|
cd backend
|
||||||
python app.py $data_dir
|
python app.py $data_dir
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in new issue
Block a user