From 4b33915c7dbacc9e6c5db1a8ce5ff1f226d935c2 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Sat, 9 May 2026 08:55:58 -0400 Subject: [PATCH] prep work for sessionUpdated and chat session auto-naming --- gadget-code/src/lib/code-session.ts | 47 +++++++++- gadget-code/src/services/chat-session.ts | 111 +++++++++++++++++++---- 2 files changed, 139 insertions(+), 19 deletions(-) diff --git a/gadget-code/src/lib/code-session.ts b/gadget-code/src/lib/code-session.ts index 9cc0361..a649a8c 100644 --- a/gadget-code/src/lib/code-session.ts +++ b/gadget-code/src/lib/code-session.ts @@ -21,6 +21,8 @@ import { SubmitPromptCallback, } from "@gadget/api"; +import ChatSession from "../models/chat-session.ts"; + import { ChatSessionService, SocketService } from "../services/index.ts"; export class CodeSession extends SocketSession { @@ -187,16 +189,47 @@ export class CodeSession extends SocketSession { this.chatSession, content, ); - this.currentTurnId = turn._id; - this.log.info("ChatTurn created", { turnId: turn._id, chatSessionId: this.chatSession._id, }); + this.currentTurnId = turn._id; droneSession.setCurrentTurnId(turn._id); + + /* + * Increment the chat session turn count and replace our cached view of + * it. Reject the prompt if the chat session has been removed. + */ + const newSession = await ChatSession.findOneAndUpdate( + { _id: this.chatSession._id }, + { $inc: { "stats.turnCount": 1 } }, + { + new: true, + populate: ChatSessionService.populateChatSession, + }, + ); + if (!newSession) { + // remove the turn + await ChatSessionService.delete(this.chatSession._id); + + // reject the prompt + const error = new Error("chat session has been removed"); + error.statusCode = 404; + throw error; + } + this.chatSession = newSession; + + /* + * Signal the IDE that the turn was created successfully. This moves the + * IDE to the Processing state, and it will start expecting events to be + * streamed in from the drone while processing the prompt. + */ cb(true, { turnId: turn._id, message: "turn created successfully" }); + /* + * Forward to gadget-drone as a work order for processing. + */ droneSession.socket.emit( "processWorkOrder", this.selectedDrone, @@ -218,7 +251,17 @@ export class CodeSession extends SocketSession { } }, ); + + /* + * Call out to have the session's name auto-generated from this prompt + * if this is the first prompt. + */ + this.chatSession = await ChatSessionService.generateSessionNameFromPrompt( + this.chatSession, + content, + ); } catch (error) { + this.log.error("prompt rejected", { error }); cb(false, {}); } } diff --git a/gadget-code/src/services/chat-session.ts b/gadget-code/src/services/chat-session.ts index bee0e81..0e67274 100644 --- a/gadget-code/src/services/chat-session.ts +++ b/gadget-code/src/services/chat-session.ts @@ -2,7 +2,7 @@ // Copyright (C) 2026 Robert Colbert // All Rights Reserved -import env from "../config/env.js"; +import env from "../config/env.ts"; import path from "node:path"; import fs from "node:fs"; @@ -19,16 +19,36 @@ import { ReasoningEffort, } from "@gadget/api"; -import { DtpService } from "../lib/service.js"; -import { PopulateOptions } from "mongoose"; -import ChatSession from "../models/chat-session.js"; -import ChatTurn from "../models/chat-turn.js"; -import Project from "../models/project.js"; -import AiProvider from "../models/ai-provider.js"; import { IAiProvider } from "@gadget/api"; +import Project from "../models/project.js"; +import ChatTurn from "../models/chat-turn.js"; +import ChatSession from "../models/chat-session.js"; +import AiProvider from "../models/ai-provider.js"; + +import { DtpService } from "../lib/service.js"; +import { PopulateOptions } from "mongoose"; +import { + AiApi, + createAiApi, + IAiEnvironment, + IAiProvider as IAiApiProvider, +} from "@gadget/ai"; + +const aiEnv: IAiEnvironment = { + NODE_ENV: env.NODE_ENV || "develop", + services: { + google: { + cse: { + apiKey: env.google.cse.apiKey, + engineId: env.google.cse.engineId, + }, + }, + }, +}; + class ChatSessionService extends DtpService { - private populateSession: PopulateOptions[] = [ + public populateChatSession: PopulateOptions[] = [ { path: "user", select: "-passwordSalt -password" }, { path: "project", @@ -41,7 +61,7 @@ class ChatSessionService extends DtpService { }, { path: "provider" }, ]; - private populateChatTurn: PopulateOptions[] = [ + public populateChatTurn: PopulateOptions[] = [ { path: "user", select: "-passwordSalt -password", @@ -51,7 +71,7 @@ class ChatSessionService extends DtpService { }, { path: "session", - populate: this.populateSession, + populate: this.populateChatSession, }, { path: "provider", @@ -123,7 +143,7 @@ class ChatSessionService extends DtpService { model: selectedModel, }); - return session.populate(this.populateSession); + return session.populate(this.populateChatSession); } /** @@ -368,12 +388,69 @@ class ChatSessionService extends DtpService { /** * Generates a session name from the first prompt. */ - generateSessionNameFromPrompt(prompt: string): string { - // Take first 50 chars, remove special chars, capitalize - const truncated = prompt.slice(0, 50).replace(/[^a-zA-Z0-9\s]/g, ""); - const words = truncated.trim().split(/\s+/); - const name = words.slice(0, 5).join(" "); - return name.charAt(0).toUpperCase() + name.slice(1); + async generateSessionNameFromPrompt( + session: IChatSession, + prompt: string, + ): Promise { + const dbProvider: IAiProvider = session.provider as IAiProvider; + const provider: IAiApiProvider = this.mapDbProviderToConfig(dbProvider); + + this.log.info("calling provider to generate chat session title", { + provider: { + _id: provider._id, + name: provider.name, + }, + selectedModel: session.selectedModel, + }); + + const api: AiApi = createAiApi(aiEnv, provider, this.log); + const response = await api.generate( + { + provider, + modelId: session.selectedModel, + params: { + reasoning: false, + temperature: 1.0, + topK: 0.6, + topP: 0.4, + }, + }, + { + systemPrompt: + "You are an assistant that creates titles for chat sessions by examining the first prompt.", + prompt: `The first prompt submitted by the user: \n\n${prompt}`, + }, + ); + + const newSession = await ChatSession.findOneAndUpdate( + { _id: session._id }, + { $set: { name: response.response || "New Session" } }, + { new: true, populate: this.populateChatSession, lean: true }, + ); + if (!newSession) { + const error = new Error("chat session has been removed"); + error.statusCode = 404; + throw error; + } + + //TODO: emit the `sessionUpdated` message to the CodeSession in the IDE + // for this chat session, letting it know the name of the chat session has + // changed. The IDE will then update it's displays and state for the User. + + return newSession; + } + + mapDbProviderToConfig(provider: IAiProvider | GadgetId): IAiApiProvider { + if (typeof provider === "string") { + throw new Error("Provider must be populated, not a GadgetId reference"); + } + return { + _id: provider._id, + name: provider.name, + sdk: provider.apiType, // map apiType → sdk + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + }; } }