| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using InvercargillSql.Orm;
- using Inversion;
- using ClankerSaddle.Data;
- namespace ClankerSaddle.Services {
- /**
- * Advisor chat storage (goal 27): the CRUD the chat pages and
- * actions sit on — create/delete/list chats (private per user:
- * every read takes the owning user_id and returns nothing for
- * anyone else), the message transcript, and the suggestion rows.
- * The TURN LOOP itself lives in {@link AdvisorService}, which owns
- * message appends that must bump the pulse; everything the UI
- * needs to RENDER (including recording plain user/system messages
- * on behalf of actions) goes through here.
- */
- public class ChatService : Object {
- private OrmSession db = inject<OrmSession>();
- /**
- * Creates a chat titled `title` (trimmed; empty falls back to
- * "New chat") owned by `user_id`, starting idle.
- */
- public async AdvisorChat create_chat(int64 user_id, string title) throws Error {
- string trimmed = title.strip();
- var now = new DateTime.now_local();
- var chat = new AdvisorChat() {
- user_id = user_id,
- title = trimmed.length > 0 ? trimmed : "New chat",
- status = "idle",
- created_at = now,
- updated_at = now
- };
- yield db.insert_async<AdvisorChat>(chat);
- return chat;
- }
- /**
- * Deletes the chat owned by `user_id` (idempotent for anyone
- * else's id): the row plus its messages and suggestions.
- */
- public async void delete_chat(int64 id, int64 user_id) throws Error {
- var chat = yield get_chat(id, user_id);
- if (chat == null) {
- return;
- }
- foreach (var message in yield list_messages(id)) {
- yield db.delete_async<AdvisorMessage>(message);
- }
- foreach (var suggestion in yield list_suggestions(id)) {
- yield db.delete_async<AdvisorSuggestion>(suggestion);
- }
- yield db.delete_async<AdvisorChat>((!)chat);
- }
- /**
- * The chat owned by `user_id`, or null when the id is unknown
- * or belongs to someone else — chats are private per user.
- */
- public async AdvisorChat? get_chat(int64 id, int64 user_id) throws Error {
- return yield db.query<AdvisorChat>()
- .where(expr("id == $0 && user_id == $1",
- elem<int64?>(id), elem<int64?>(user_id)))
- .first_async();
- }
- /** The user's chats, newest first. */
- public async ImmutableLot<AdvisorChat> list_chats(int64 user_id) throws Error {
- return yield db.query<AdvisorChat>()
- .where(expr("user_id == $0", elem<int64?>(user_id)))
- .order_by_desc(expr("updated_at"))
- .order_by_desc(expr("id"))
- .materialise_async();
- }
- /** One chat's transcript, oldest first. */
- public async ImmutableLot<AdvisorMessage> list_messages(int64 chat_id) throws Error {
- return yield db.query<AdvisorMessage>()
- .where(expr("chat_id == $0", elem<int64?>(chat_id)))
- .order_by(expr("id"))
- .materialise_async();
- }
- /** One chat's suggestions, oldest first. */
- public async ImmutableLot<AdvisorSuggestion> list_suggestions(int64 chat_id) throws Error {
- return yield db.query<AdvisorSuggestion>()
- .where(expr("chat_id == $0", elem<int64?>(chat_id)))
- .order_by(expr("id"))
- .materialise_async();
- }
- /**
- * Appends a message row verbatim (the advisor loop's writer;
- * actions use it for user/system rows) and touches the chat's
- * updated_at. `payload` is JSON or null.
- */
- public async AdvisorMessage append_message(int64 chat_id, string kind,
- string content, string? payload) throws Error {
- var message = new AdvisorMessage() {
- chat_id = chat_id,
- kind = kind,
- content = content,
- payload = payload,
- created_at = new DateTime.now_local()
- };
- yield db.insert_async<AdvisorMessage>(message);
- yield touch_chat(chat_id);
- return message;
- }
- /** Records a plain user message (no payload). */
- public async AdvisorMessage record_user_message(int64 chat_id,
- string content) throws Error {
- return yield append_message(chat_id, "user", content, null);
- }
- /** Records an advisor-external system notice (task callbacks). */
- public async AdvisorMessage record_system_message(int64 chat_id,
- string content) throws Error {
- return yield append_message(chat_id, "system", content, null);
- }
- /** Bumps the chat's updated_at (every append's trailer). */
- public async void touch_chat(int64 chat_id) throws Error {
- var chat = yield db.query<AdvisorChat>()
- .where(expr("id == $0", elem<int64?>(chat_id)))
- .first_async();
- if (chat != null) {
- ((!)chat).updated_at = new DateTime.now_local();
- yield db.update_async<AdvisorChat>((!)chat);
- }
- }
- /**
- * Sets the chat's loop status ('idle' / 'running' /
- * 'awaiting_answer') and touches updated_at.
- */
- public async void set_chat_status(int64 chat_id, string status) throws Error {
- var chat = yield db.query<AdvisorChat>()
- .where(expr("id == $0", elem<int64?>(chat_id)))
- .first_async();
- if (chat != null) {
- ((!)chat).status = status;
- ((!)chat).updated_at = new DateTime.now_local();
- yield db.update_async<AdvisorChat>((!)chat);
- }
- }
- /**
- * The chat's newest unanswered question row (an assistant
- * message whose payload is a question object), or null.
- */
- public async AdvisorMessage? pending_question(int64 chat_id) throws Error {
- AdvisorMessage? found = null;
- foreach (var message in yield list_messages(chat_id)) {
- if (message.kind == "assistant" && is_question(message)
- && message.answer == null) {
- found = message;
- }
- }
- return found;
- }
- /** True when `message` is a parked ask_question row. */
- public static bool is_question(AdvisorMessage message) {
- if (message.payload == null) {
- return false;
- }
- var parser = new Json.Parser();
- try {
- parser.load_from_data(((!)message.payload));
- } catch (Error e) {
- return false;
- }
- var root = parser.get_root();
- if (root == null || root.get_node_type() != Json.NodeType.OBJECT) {
- return false;
- }
- return ((!)root).get_object().has_member("question");
- }
- }
- }
|