Temporary agents and chats

This commit is contained in:
Shivering Ocelot committed 2026-04-16 15:16:03 -05:00
1 parent fe596761dc
commit e07f8ee05f
15 files changed
+367 -92

No files matched your search

+3
View File
@@ -6,3 +6,6 @@ horde.db
/test-data /test-data
/local-data /local-data
/models /models
package-lock.json
node_modules/
+7 -20
View File
@@ -51,10 +51,8 @@ class ExpertAgent(Agent):
if prompt: if prompt:
messages.append(LlamaMessage("user", prompt)) messages.append(LlamaMessage("user", prompt))
model = self.config['model'] model_id = self.config['model']
db_model = await self.app.get_model(model_id)
if type(model) is int:
db_model = await self.app.get_model(model)
model_engine = db_model.value["engine"] model_engine = db_model.value["engine"]
opts = {} opts = {}
@@ -70,30 +68,17 @@ class ExpertAgent(Agent):
model=db_model.value['model'], model=db_model.value['model'],
opts=opts, opts=opts,
) )
return response
elif model_engine == "replicate": elif model_engine == "replicate":
if 'parameters' in db_model: if 'parameters' in db_model:
opts['parameters'] = db_model['parameters'] opts['parameters'] = db_model['parameters']
response = await inference.replicate_completion(self.app, messages, model=db_model.value['model'], opts=opts) response = await inference.replicate_completion(self.app, messages, model=db_model.value['model'], opts=opts)
elif model_engine == "ollama":
response = await inference.ollama_chat_completion(self.app, messages, model=db_model.value['model'], opts=opts)
else: else:
raise Exception("Unsuported model: " + repr(db_model.value)) raise Exception("Unsuported model: " + repr(db_model.value))
elif model.startswith("ollama:"):
response = await inference.ollama_chat_completion(self.app, messages, model=model[7:])
elif model == "qwen-big":
response = await inference.replicate_qwen_completion(self.app, messages, model=inference.Models.qwen3_235b)
elif model == "qwen-small":
response = await inference.replicate_qwen_completion(self.app, messages, model=inference.Models.qwen3_8b)
elif model == "llama-big":
response = await inference.replicate_llama_completion(self.app, messages, model=inference.Models.llama_70b)
elif model == "llama-small":
response = await inference.replicate_llama_completion(self.app, messages, model=inference.Models.llama_8b)
else:
# An error here is questionable, model is user configurable
raise Exception("Unkonwn agent model: " + model)
return response return response
async def received_message(self, message): async def received_message(self, message):
@@ -108,6 +93,8 @@ class ExpertAgent(Agent):
return 'assistant' return 'assistant'
elif sender == 'user': elif sender == 'user':
return 'user' return 'user'
elif sender.startswith("tmp-"):
return 'assistant'
else: else:
raise Exception("Unsupported sender: " + sender) raise Exception("Unsupported sender: " + sender)
+43 -10
View File
@@ -13,6 +13,7 @@ import replicate
import database as db import database as db
import agent as agent_mod import agent as agent_mod
import agents as agents_mod import agents as agents_mod
import temporary as tmp
from server import start_server from server import start_server
from chat import ChatView, Message, MESSAGE_PAGE_SIZE from chat import ChatView, Message, MESSAGE_PAGE_SIZE
@@ -25,6 +26,8 @@ class App(ChatMixin):
self.remote_connections = {} self.remote_connections = {}
self.streams = {} self.streams = {}
self.tmp = tmp.State()
# Clients # Clients
self.qdrant_client = QdrantClient(path=f"{self.data_directory}/qdrant_data") self.qdrant_client = QdrantClient(path=f"{self.data_directory}/qdrant_data")
@@ -67,9 +70,41 @@ class App(ChatMixin):
# --- Agent Methods (Asynchronous) --- # --- Agent Methods (Asynchronous) ---
async def create_agent(self, user_id, name, agentname, description, picture, kind, data, main_chat_id=None): async def get_agent(self, agent_id):
if tmp.is_tmp_id(agent_id):
agent = self.tmp.agents.get(agent_id)
else:
agent = await db.Agent.get_or_none(id=agent_id)
if not agent:
raise Exception(f"Agent not found with id {agent_id}")
return agent
async def create_agent(self, user_id, name, agentname, description, picture, kind, data, main_chat_id=None, temporary=False):
print("create_agent; user_id, name:", user_id, name) print("create_agent; user_id, name:", user_id, name)
if temporary:
agent = tmp.Agent.create(
self,
user_id=user_id,
name=name,
agentname=agentname,
main_chat_id=main_chat_id,
description=description,
picture=picture,
kind=kind,
data=data or {}
)
if main_chat_id is not None:
if tmp.is_tmp_id(main_chat_id):
self.tmp.chats[main_chat_id].participants.append(agent.id)
else:
print(f"ERROR: cannot use {main_chat_id} as main chat for temporary agent {agentname or agent.id}")
agent.main_chat_id = None
return agent.id
if main_chat_id is None: if main_chat_id is None:
chat = await db.Chat.create( chat = await db.Chat.create(
user_id=user_id, user_id=user_id,
@@ -97,9 +132,7 @@ class App(ChatMixin):
return agent.id return agent.id
async def update_agent(self, agent_id, name=None, description=None, picture=None, agentname=None, data=None): async def update_agent(self, agent_id, name=None, description=None, picture=None, agentname=None, data=None):
agent = await db.Agent.get_or_none(id=agent_id) agent = await self.get_agent(agent_id)
if not agent:
raise Exception(f"Agent not found with id {agent_id}")
summary = [] summary = []
if name is not None: if name is not None:
@@ -120,11 +153,15 @@ class App(ChatMixin):
agent.data.update(data) agent.data.update(data)
summary.extend([f"data.{k}" for k in data.keys()]) summary.extend([f"data.{k}" for k in data.keys()])
if db.is_instance(agent):
await agent.save() await agent.save()
print(f"Updated agent #{agent.id} fields: {summary}") print(f"Updated agent #{agent.id} fields: {summary}")
async def get_agents(self, user_id): async def get_agents(self, user_id):
agents = await db.Agent.filter(user_id=user_id).all() agents = await db.Agent.filter(user_id=user_id).all()
if user_id in self.tmp.user_agents:
agents += [self.tmp.agents[agent_id] for agent_id in self.tmp.user_agents[user_id]]
return [{ return [{
"id": agent.id, "id": agent.id,
"name": agent.name, "name": agent.name,
@@ -176,9 +213,7 @@ class App(ChatMixin):
# --- Agent Control & Actions --- # --- Agent Control & Actions ---
async def get_agent_control(self, agent_id): async def get_agent_control(self, agent_id):
agent = await db.Agent.get_or_none(id=agent_id) agent = await self.get_agent(agent_id)
if not agent:
raise Exception(f"Agent not found with id {agent_id}")
agent_class = next((kind for kind in self.agent_classes if kind.identifier == agent.kind), None) agent_class = next((kind for kind in self.agent_classes if kind.identifier == agent.kind), None)
if agent_class is None: if agent_class is None:
@@ -189,9 +224,7 @@ class App(ChatMixin):
return control return control
async def get_agent_config_fields(self, agent_id): async def get_agent_config_fields(self, agent_id):
agent = await db.Agent.get_or_none(id=agent_id) agent = await self.get_agent(agent_id)
if not agent:
raise Exception(f"Agent not found with id {agent_id}")
agent_class = next((it for it in self.agent_classes if it.identifier == agent.kind), None) agent_class = next((it for it in self.agent_classes if it.identifier == agent.kind), None)
if not agent_class: if not agent_class:
+60 -2
View File
@@ -2,12 +2,21 @@ import asyncio
from datetime import datetime from datetime import datetime
import database as db import database as db
import temporary as tmp
from chat import ChatView, Message, MESSAGE_PAGE_SIZE from chat import ChatView, Message, MESSAGE_PAGE_SIZE
from stream import Stream from stream import Stream
class ChatMixin: class ChatMixin:
async def create_chat(self, user_id, name, description, picture, members=[]): async def create_chat(self, user_id, name, description, picture, members=[], temporary=False):
# Tortoise uses .create() for one-line instantiation and saving if temporary:
chat = tmp.Chat.create(
self,
user_id=user_id,
name=name,
description=description,
picture=picture
)
else:
chat = await db.Chat.create( chat = await db.Chat.create(
user_id=user_id, user_id=user_id,
name=name, name=name,
@@ -24,6 +33,11 @@ class ChatMixin:
async def get_chats(self, user_id): async def get_chats(self, user_id):
# Direct filtering on the model class # Direct filtering on the model class
chats = await db.Chat.filter(user_id=user_id).all() chats = await db.Chat.filter(user_id=user_id).all()
tmp_chats = self.tmp.get_user_chats(user_id)
if tmp_chats:
chats += [self.tmp.chats[chat_id] for chat_id in tmp_chats]
return [{ return [{
"id": chat.id, "id": chat.id,
"name": chat.name, "name": chat.name,
@@ -32,6 +46,11 @@ class ChatMixin:
} for chat in chats] } for chat in chats]
async def delete_chat(self, chat_id): async def delete_chat(self, chat_id):
if tmp.is_tmp_id(chat_id):
chat = self.tmp.chats[chat_id]
# TODO: Delete
else:
# Filtering and deleting is a one-liner # Filtering and deleting is a one-liner
await db.Membership.filter(chat_id=chat_id).delete() await db.Membership.filter(chat_id=chat_id).delete()
await db.Chat.filter(id=chat_id).delete() await db.Chat.filter(id=chat_id).delete()
@@ -39,6 +58,11 @@ class ChatMixin:
print(f"Deleted chat #{chat_id} and its memberships.") print(f"Deleted chat #{chat_id} and its memberships.")
async def get_chat_messages(self, chat_id, earliest_index=None): async def get_chat_messages(self, chat_id, earliest_index=None):
if tmp.is_tmp_id(chat_id):
chat = self.tmp.chats[chat_id]
return [message.to_json() for message in chat.messages]
query = db.Message.filter(chat_id=chat_id) query = db.Message.filter(chat_id=chat_id)
if earliest_index is not None: if earliest_index is not None:
query = query.filter(id__gt=earliest_index) query = query.filter(id__gt=earliest_index)
@@ -58,6 +82,9 @@ class ChatMixin:
} for message in messages] } for message in messages]
async def create_chat_thread(self, chat_id, name=None): async def create_chat_thread(self, chat_id, name=None):
if tmp.is_tmp_id(chat_id):
thread = tmp.Thread.create(self, chat_id, name)
else:
thread = await db.Thread.create( thread = await db.Thread.create(
chat_id=chat_id, chat_id=chat_id,
name=name name=name
@@ -66,6 +93,10 @@ class ChatMixin:
return thread.id return thread.id
async def add_agent_to_chat(self, chat_id, agent_id): async def add_agent_to_chat(self, chat_id, agent_id):
if tmp.is_tmp_id(chat_id):
chat = self.tmp.chats[chat_id]
chat.participants.append(agent_id)
else:
await db.Membership.create( await db.Membership.create(
chat_id=chat_id, chat_id=chat_id,
agent_id=agent_id agent_id=agent_id
@@ -82,6 +113,24 @@ class ChatMixin:
# Set to true for streams # Set to true for streams
updating = False updating = False
if tmp.is_tmp_id(chat_id):
message = Message(
tmp.generate_tmp_id(self),
#chat_id=chat_id,
time=now,
#last_edit=now,
sender=sender,
kind=kind or 'message',
subkind=subkind,
#format=format,
content=initial_content,
thread_id=thread_id
)
message.chat_id = chat_id
chat = self.tmp.chats[chat_id]
chat.messages.append(message)
chat_owner_id = chat.user_id
else:
message = await db.Message.create( message = await db.Message.create(
chat_id=chat_id, chat_id=chat_id,
time=now, time=now,
@@ -102,7 +151,11 @@ class ChatMixin:
async def notify_agents(): async def notify_agents():
# Fetch participant agents # Fetch participant agents
if tmp.is_tmp_id(chat_id):
agent_ids = self.tmp.chats[chat_id].participants
else:
agent_ids = await db.Membership.filter(chat_id=chat_id).values_list("agent_id", flat=True) agent_ids = await db.Membership.filter(chat_id=chat_id).values_list("agent_id", flat=True)
for agent_id in agent_ids: for agent_id in agent_ids:
if str(agent_id) != str(sender): if str(agent_id) != str(sender):
await self.notify_message_to_agent(agent_id, message) await self.notify_message_to_agent(agent_id, message)
@@ -120,6 +173,7 @@ class ChatMixin:
notify_users() notify_users()
async def end_fn (): async def end_fn ():
message.content = content.content message.content = content.content
if db.is_instance(message):
await message.save() await message.save()
await notify_agents() await notify_agents()
notify_users() notify_users()
@@ -142,6 +196,9 @@ class ChatMixin:
} }
async def update_chat_message(self, message_id, content=None, kind=None, subkind=None, format=None): async def update_chat_message(self, message_id, content=None, kind=None, subkind=None, format=None):
if tmp.is_tmp_id(message_id):
raise Exception("Cannot update temporary messages")
else:
message = await db.Message.get_or_none(id=message_id) message = await db.Message.get_or_none(id=message_id)
if not message: if not message:
@@ -161,6 +218,7 @@ class ChatMixin:
summary.append('format') summary.append('format')
if summary: if summary:
if db.is_instance(message):
await message.save() # Saves the specific instance await message.save() # Saves the specific instance
print(f"Updated message #{message.id} fields: {summary}") print(f"Updated message #{message.id} fields: {summary}")
+30 -3
View File
@@ -3,17 +3,19 @@ from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select from sqlalchemy.sql.expression import select
import database as db import database as db
import temporary as tmp
MESSAGE_PAGE_SIZE = 256 MESSAGE_PAGE_SIZE = 256
class Message: class Message:
def __init__(self, id, content, sender, time=None, kind='message', subkind=None): def __init__(self, id, content, sender, time=None, kind='message', subkind=None, thread_id=None):
self.id = id self.id = id
self.content = content self.content = content
self.sender = sender self.sender = sender
self.time = time self.time = time
self.kind = kind self.kind = kind
self.subkind = subkind self.subkind = subkind
self.thread_id = thread_id
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)
@@ -24,6 +26,16 @@ class Message:
def is_from_agent(self, agent_id): def is_from_agent(self, agent_id):
return self.sender == str(agent_id) return self.sender == str(agent_id)
def to_json(self):
return {
"id": self.id,
"time": self.time,
"sender": self.sender,
"kind": self.kind,
"content": self.content,
"threadId": self.thread_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")
@@ -67,12 +79,16 @@ class ChatView:
self.current_thread_id = None self.current_thread_id = None
self._earliest_message_id = None self._earliest_message_id = None
self._earliest_message_time = None
async def init_load(self): async def init_load(self):
await self._load_basic_data() await self._load_basic_data()
await self._load_next_messages(MESSAGE_PAGE_SIZE) await self._load_next_messages(MESSAGE_PAGE_SIZE)
async def _load_basic_data(self): async def _load_basic_data(self):
if tmp.is_tmp_id(self.chat_id):
record = self.app.tmp.chats.get(self.chat_id)
else:
# Using get_or_none for cleaner existence checks # Using get_or_none for cleaner existence checks
record = await db.Chat.get_or_none(id=self.chat_id) record = await db.Chat.get_or_none(id=self.chat_id)
@@ -83,6 +99,15 @@ class ChatView:
self.description = record.description self.description = record.description
async def _load_next_messages(self, load_count): async def _load_next_messages(self, load_count):
if tmp.is_tmp_id(self.chat_id):
chat = self.app.tmp.chats[self.chat_id]
record_batch = [
msg for msg in chat.messages
if not self._earliest_message_time or msg.id < self._earliest_message_time
][:load_count]
record_batch.reverse()
records_rev = record_batch
else:
# Start building the filter # Start building the filter
filters = {"chat_id": self.chat_id} filters = {"chat_id": self.chat_id}
@@ -96,8 +121,10 @@ class ChatView:
# Update the earliest id for the next load operation # Update the earliest id for the next load operation
if records_rev: if records_rev:
# Note: Since we ordered by -time (descending), index 0 is the newest in this batch # Note: Since we ordered by -time (descending), index 0 is the newest in this batch (highest id/time)
self._earliest_message_id = records_rev[-1].id # The "earliest" is the one with the smallest ID/Time in this set # The "earliest" or oldest is the one with the smallest ID/Time in this set
self._earliest_message_id = records_rev[-1].id
self._earliest_message_time = records_rev[-1].time
if not self.threads: if not self.threads:
# current thread is the latest known from the newest record in batch # current thread is the latest known from the newest record in batch
+4
View File
@@ -29,6 +29,10 @@ async def init_database (data_directory):
) )
await Tortoise.generate_schemas() await Tortoise.generate_schemas()
# Checks whether an object is an instance of any database model
def is_instance (value):
isinstance(value, models.Model)
class User(models.Model): class User(models.Model):
id = fields.IntField(pk=True) id = fields.IntField(pk=True)
email = fields.CharField(max_length=255) email = fields.CharField(max_length=255)
+27 -15
View File
@@ -1,4 +1,5 @@
import threading import threading
import asyncio
import ollama import ollama
from stream import Stream from stream import Stream
@@ -23,22 +24,33 @@ class Models:
# llama cpp models # llama cpp models
model_cache = {} model_cache = {}
last_model_file = None
last_model_instance = None
def get_llama_cpp_model(app, model_file):
global last_model_file, last_model_instance
if last_model_file == model_file:
return last_model_instance
if last_model_instance is not None:
#last_model_instance = None
del last_model_instance
last_model_file = model_file
last_model_instance = Llama(
model_path=f"{app.data_directory}/models/{model_file}",
n_gpu_layers=-1, # Uncomment to use GPU acceleration
# seed=1337, # Uncomment to set a specific seed
n_ctx=CONTEXT_WINDOW,
)
return last_model_instance
# llama.cpp # llama.cpp
async def local_chat_completion(app, messages, model, opts={}): async def local_chat_completion(app, messages, model, opts={}):
content_stream = Stream(app) content_stream = Stream(app)
def worker(): def worker():
if model in model_cache: llm = get_llama_cpp_model(app, model)
llm = model_cache[model]
else:
llm = Llama(
model_path=f"{app.data_directory}/models/{model}",
n_gpu_layers=-1, # Uncomment to use GPU acceleration
# seed=1337, # Uncomment to set a specific seed
n_ctx=CONTEXT_WINDOW,
)
model_cache[model] = llm
temp = opts.get("temperature") temp = opts.get("temperature")
if temp is None: if temp is None:
@@ -77,6 +89,7 @@ async def ollama_list(app, messages, model, opts={}):
async def ollama_chat_completion(app, messages, model, opts={}): async def ollama_chat_completion(app, messages, model, opts={}):
content_stream = Stream(app) content_stream = Stream(app)
async def worker():
print("START OLLAMA INFERENCE") print("START OLLAMA INFERENCE")
stream_response = await ollama.AsyncClient().chat( stream_response = await ollama.AsyncClient().chat(
model=model, model=model,
@@ -86,14 +99,13 @@ async def ollama_chat_completion(app, messages, model, opts={}):
async for part in stream_response: async for part in stream_response:
content_stream.feed(part.message.content) content_stream.feed(part.message.content)
print(part['message']['content'], end='', flush=True)
content_stream.close() content_stream.close()
print("END OLLAMA INFERENCE") print("END OLLAMA INFERENCE")
return await content_stream.content_wait()
#return content_stream asyncio.create_task(worker())
return content_stream
async def replicate_completion(app, messages, model=None, opts={}): async def replicate_completion(app, messages, model=None, opts={}):
@@ -104,7 +116,7 @@ async def replicate_completion(app, messages, model=None, opts={}):
arguments = {} arguments = {}
arguments[parameter_names.get('prompt', 'prompt')] = format_instruct(messages) arguments[parameter_names.get('prompt', 'prompt')] = format_instruct(messages)
arguments[parameter_names.get('temperature', 'temperature')] = opts.get('temperature', 0.75) 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('max_tokens', 'max_tokens')] = opts.get('max_tokens', 2048)
arguments[parameter_names.get('system_prompt', 'system_prompt')] = "" arguments[parameter_names.get('system_prompt', 'system_prompt')] = ""
+3 -1
View File
@@ -162,7 +162,8 @@ def start_server(app):
description=body.get('description'), description=body.get('description'),
picture=body.get('picture'), picture=body.get('picture'),
kind=body['kind'], kind=body['kind'],
data=body.get('data') data=body.get('data'),
temporary=body.get('temporary'),
) )
await send({'message': "agent created", 'agentId': agent_id, 'nonce': nonce}) await send({'message': "agent created", 'agentId': agent_id, 'nonce': nonce})
@@ -216,6 +217,7 @@ def start_server(app):
description=body.get('description'), description=body.get('description'),
picture=body.get('picture'), picture=body.get('picture'),
members=body.get('members', []), members=body.get('members', []),
temporary=body.get('temporary'),
) )
await send({'message': "chat created", 'chatId': chat_id, 'nonce': nonce}) await send({'message': "chat created", 'chatId': chat_id, 'nonce': nonce})
+87
View File
@@ -0,0 +1,87 @@
def generate_tmp_id(app):
app.tmp.id_counter += 1
return "tmp-" + str(app.tmp.id_counter)
def is_tmp_id(id):
return type(id) == str and id.startswith("tmp-")
class State:
def __init__(self):
self.id_counter = 0
self.agents = {}
self.chats = {}
self.threads = {}
# list of temporary agent ids that belong to a real user
self.user_agents = {}
# list of temporary chat ids that belong to a real user
self.user_chats = {}
# list of temporary chat ids where a real agent is participating
self.participating = {}
def get_user_agents(self, user_id):
if user_id not in self.user_agents:
self.user_agents[user_id] = []
return self.user_agents[user_id]
def get_user_chats(self, user_id):
if user_id not in self.user_chats:
self.user_chats[user_id] = []
return self.user_chats[user_id]
class Agent:
def create(app, user_id, name, agentname, main_chat_id, description="", picture=None, kind='default', data={}):
self = Agent()
self.id = None
self.user_id = user_id
self.name = name
self.agentname = agentname
self.main_chat_id = main_chat_id
self.description = description
self.picture = picture
self.kind = kind
self.data = data
self.id = generate_tmp_id(app)
app.tmp.agents[self.id] = self
app.tmp.get_user_agents(user_id).append(self.id)
return self
class Chat:
def create(app, user_id, name, description="", picture=None, ordering=None):
self = Chat()
self._app = app
self.id = None
self.user_id = user_id
self.name = name
self.description = description
self.picture = picture
self.ordering = ordering
self.messages = []
self.threads = []
self.participants = []
self.id = generate_tmp_id(app)
app.tmp.chats[self.id] = self
app.tmp.get_user_chats(user_id).append(self.id)
return self
class Thread:
def create(app, chat_id, name):
self = Thread()
self.chat_id = chat_id
self.name = name
self.id = generate_tmp_id(app)
app.tmp.threads[self.id] = self
app.tmp.chats[chat_id].threads.append(self.id)
return self
+47 -28
View File
@@ -32,6 +32,10 @@
payload.identifier = agent.value.identifier; payload.identifier = agent.value.identifier;
} }
if (agent.value.temporary) {
payload.temporary = true;
}
let response = await backend.sendAction("create-agent", payload); let response = await backend.sendAction("create-agent", payload);
if (response.error) { if (response.error) {
@@ -54,39 +58,54 @@
<h1>Create Agent</h1> <h1>Create Agent</h1>
</TitleBar> </TitleBar>
<div class="main-container"> <section class="container">
<input v-model="agent.name" type="text" placeholder="name"> <input v-model="agent.name" placeholder="Name" />
<input v-model="agent.identifier" type="text" placeholder="identifier"> <input v-model="agent.identifier" placeholder="Identifier (optional)" />
<select v-model="agent.kind"> <select v-model="agent.kind" :class="{ placeholder: !agent.kind }">
<option v-for="k in kinds" :value="k">{{k}}</option> <option value="" disabled>Select type</option>
<option v-for="k in kinds" :key="k" :value="k">{{ k }}</option>
</select> </select>
<label class="checkbox">
<button @click="submitAgent()">Create Agent</button> <input type="checkbox" v-model="agent.temporary" />
</div> Temporary
</label>
<button @click="submitAgent()">Create</button>
</section>
</main> </main>
</template> </template>
<style scoped> <style scoped>
.main-container { section {
display: flex; margin: 1em;
flex-direction: column; padding: 1em;
}
.multi-select-box {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
.container {
.multi-select-box div { display: flex;
border-bottom: 1px solid var(--color-border); flex-direction: column;
} }
.multi-select-box div:last { input, select {
border-bottom: none; padding: 10px;
} border: 1px solid var(--color-border);
.multi-select-box div:hover { border-radius: 6px;
background-color: var(--color-background-mute); font-size: 14px;
} background: var(--color-background);
.multi-select-box div.selected { }
background-color: var(--color-background-soft); input::placeholder, select.placeholder {
} color: var(--color-text-light);
}
input:focus, select:focus { outline: none; border-color: var(--color-primary); }
.checkbox { display: flex; align-items: center; gap: 6px; cursor: pointer; }
button {
margin-top: 4px;
padding: 10px;
background: var(--color-primary);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
button:hover { opacity: 0.9; }
</style> </style>
+18 -6
View File
@@ -59,11 +59,9 @@
// === State Management === // // === State Management === //
async function loadChatState (newId) { async function loadChatState (newId) {
chatId = parseInt(newId); // This line was converting newId into an integer.
if (chatId == null || isNaN(chatId)) { // Some parts of the program may still rely on it.
console.error("Invalid chat id", chatId); chatId = newId;
return;
}
// Create an agent name dictionary // Create an agent name dictionary
const _agents = await backend.getAgents(); const _agents = await backend.getAgents();
@@ -307,7 +305,7 @@
async function startNewThread () { async function startNewThread () {
const res = await backend.sendAction("create-chat-thread", {chatId}); const res = await backend.sendAction("create-chat-thread", {chatId});
console.log("Starting a new thread", res); console.log("Starting a nedeleteChatw thread", res);
if (res.threadId != null) { if (res.threadId != null) {
currentThreadId.value = res.threadId; currentThreadId.value = res.threadId;
expandThread(res.threadId, true); expandThread(res.threadId, true);
@@ -325,6 +323,16 @@
} }
} }
async function deleteChat (chat) {
if (!await ui.confirm(`Delete Chat ${chat.name} with id ${chat.id}`)) return;
const res = await backend.sendAction("delete-chat", {chatId: chat.id});
console.log("Chat Delete Output", res);
}
async function renameChat (chat) {
ui.alert("TODO: Rename Chats");
}
// === Special Chat Functions === // // === Special Chat Functions === //
@@ -418,6 +426,10 @@
<main> <main>
<TitleBar> <TitleBar>
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1> <h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
<div>
<button @click="renameChat(chat)">Rename</button>
<button @click="deleteChat(chat)">Delete</button>
</div>
</TitleBar> </TitleBar>
<div class="messages-container" ref="chatContainerElem"> <div class="messages-container" ref="chatContainerElem">
+12 -2
View File
@@ -12,14 +12,20 @@
// List of agents, with their id, name, and whether they're currently selected // List of agents, with their id, name, and whether they're currently selected
const agents = ref([]); const agents = ref([]);
const title = ref(""); const title = ref("");
const temporary = ref(false);
async function submitChat () { async function submitChat () {
const selectedAgents = agents.value.filter(agent => agent.selected).map(agent => agent.id); const selectedAgents = agents.value.filter(agent => agent.selected).map(agent => agent.id);
let response = await backend.sendAction("create-chat", { let payload = {
'name': title.value, 'name': title.value,
'members': selectedAgents, 'members': selectedAgents,
}); };
if (temporary.value) {
payload.temporary = true;
}
let response = await backend.sendAction("create-chat", payload);
if (response.error) { if (response.error) {
ui.alert(`Could not create chat\n${response.error}`); ui.alert(`Could not create chat\n${response.error}`);
@@ -50,6 +56,10 @@
<div class="multi-select-box"> <div class="multi-select-box">
<div v-for="agent in agents" :class="{selected: Boolean(agent.selected)}" @click="agent.selected = !agent.selected">{{agent.name}}</div> <div v-for="agent in agents" :class="{selected: Boolean(agent.selected)}" @click="agent.selected = !agent.selected">{{agent.name}}</div>
</div> </div>
<label class="checkbox">
<input type="checkbox" v-model="temporary" />
Temporary
</label>
<button @click="submitChat()">Create Chat</button> <button @click="submitChat()">Create Chat</button>
</div> </div>
</main> </main>
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"ws": "^8.20.0"
}
}
+5 -4
View File
@@ -21,14 +21,15 @@ case "$1" in
;; ;;
"test") "test")
# Setup test data directory # Setup test data directory
mkdir -p test-data data_dir=$(realpath ./test-data)
rm -f test-data/horde.db mkdir -p $data_dir
rm -rf $data_dir/*
# Start backend in the background # Start backend in the background
python backend/src/app.py ../test-data & python backend/src/app.py $data_dir &
BACKEND_PID=$! BACKEND_PID=$!
echo "Started backend: $BACKEND_PID" echo "Started backend: $BACKEND_PID"
sleep 2 sleep 5
# Run tests # Run tests
cd testing cd testing
+16 -1
View File
@@ -1,6 +1,6 @@
const WebSocket = require('ws'); const WebSocket = require('ws');
const USE_LLM = true; const USE_LLM = false;
const hostname = 'localhost:5000'; const hostname = 'localhost:5000';
const ws = new WebSocket(`ws://${hostname}/ws`); const ws = new WebSocket(`ws://${hostname}/ws`);
@@ -144,6 +144,7 @@ async function runTest() {
)); ));
await testChats(); await testChats();
await testTemporary();
if (USE_LLM) { if (USE_LLM) {
await testGral(mainAgentId); await testGral(mainAgentId);
@@ -181,6 +182,20 @@ async function testChats () {
} }
} }
async function testTemporary () {
let createAgentResponse = await send('create-agent', {body: {
name: 'First Agent',
description: 'First agent does nothing',
kind: 'expert',
temporary: true,
data: {
system_prompt: 'you respond every message with only the world "Acknowledged"',
}
}});
console.log("Created Temporary Agent: ", createAgentResponse);
}
async function testGral (agentId) { async function testGral (agentId) {
let res = await send('call-agent-action', { let res = await send('call-agent-action', {
body: { body: {