prep work for sessionUpdated and chat session auto-naming

This commit is contained in:
Rob Colbert 2026-05-09 08:55:58 -04:00
parent d7924a9d6f
commit 4b33915c7d
2 changed files with 139 additions and 19 deletions

View File

@ -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, {});
}
}

View File

@ -2,7 +2,7 @@
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// 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<IChatSession> {
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,
};
}
}