a lot of review and by-hand cleanup (wip)
This commit is contained in:
parent
dca21cf762
commit
f35a0ce921
@ -7,6 +7,7 @@ import { Request, Response } from "express";
|
|||||||
import { DtpController } from "../../../lib/controller.js";
|
import { DtpController } from "../../../lib/controller.js";
|
||||||
import ChatSessionService from "../../../services/chat-session.js";
|
import ChatSessionService from "../../../services/chat-session.js";
|
||||||
import { ChatSessionMode } from "@gadget/api";
|
import { ChatSessionMode } from "@gadget/api";
|
||||||
|
import { populateChatSessionById } from "@/controllers/lib/populators.js";
|
||||||
|
|
||||||
class ChatSessionController extends DtpController {
|
class ChatSessionController extends DtpController {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
@ -20,20 +21,19 @@ class ChatSessionController extends DtpController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.router.get("/", this.requireUser(), this.listSessions.bind(this));
|
this.router.use(this.requireUser());
|
||||||
this.router.post("/", this.requireUser(), this.createSession.bind(this));
|
|
||||||
this.router.get("/:id", this.requireUser(), this.getSession.bind(this));
|
this.router.param("sessionId", populateChatSessionById(this));
|
||||||
this.router.put("/:id", this.requireUser(), this.updateSession.bind(this));
|
|
||||||
this.router.delete(
|
this.router.post("/", this.createSession.bind(this));
|
||||||
"/:id",
|
|
||||||
this.requireUser(),
|
this.router.get("/:sessionId/turns", this.getSessionTurns.bind(this));
|
||||||
this.deleteSession.bind(this),
|
this.router.get("/:sessionId", this.getSession.bind(this));
|
||||||
);
|
this.router.get("/", this.listSessions.bind(this));
|
||||||
this.router.get(
|
|
||||||
"/:id/turns",
|
this.router.put("/:sessionId", this.updateSession.bind(this));
|
||||||
this.requireUser(),
|
|
||||||
this.getSessionTurns.bind(this),
|
this.router.delete("/:sessionId", this.deleteSession.bind(this));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,24 +140,11 @@ class ChatSessionController extends DtpController {
|
|||||||
* GET /api/v1/chat-sessions/:id
|
* GET /api/v1/chat-sessions/:id
|
||||||
* Get a specific chat session.
|
* Get a specific chat session.
|
||||||
*/
|
*/
|
||||||
private async getSession(req: Request, res: Response): Promise<void> {
|
private async getSession(_req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id)
|
|
||||||
? req.params.id[0]
|
|
||||||
: req.params.id;
|
|
||||||
if (!id) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Session ID is required",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await ChatSessionService.getById(id);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: session,
|
data: res.locals.chatSession,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
@ -183,16 +170,6 @@ class ChatSessionController extends DtpController {
|
|||||||
*/
|
*/
|
||||||
private async updateSession(req: Request, res: Response): Promise<void> {
|
private async updateSession(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id)
|
|
||||||
? req.params.id[0]
|
|
||||||
: req.params.id;
|
|
||||||
if (!id) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Session ID is required",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const updates = req.body;
|
const updates = req.body;
|
||||||
|
|
||||||
// Validate allowed updates
|
// Validate allowed updates
|
||||||
@ -217,7 +194,10 @@ class ChatSessionController extends DtpController {
|
|||||||
ChatSessionMode[updates.mode as keyof typeof ChatSessionMode];
|
ChatSessionMode[updates.mode as keyof typeof ChatSessionMode];
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await ChatSessionService.update(id, allowedUpdates);
|
const session = await ChatSessionService.update(
|
||||||
|
res.locals.chatSession._id,
|
||||||
|
allowedUpdates,
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -245,21 +225,9 @@ class ChatSessionController extends DtpController {
|
|||||||
* DELETE /api/v1/chat-sessions/:id
|
* DELETE /api/v1/chat-sessions/:id
|
||||||
* Delete a chat session.
|
* Delete a chat session.
|
||||||
*/
|
*/
|
||||||
private async deleteSession(req: Request, res: Response): Promise<void> {
|
private async deleteSession(_req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id)
|
await ChatSessionService.delete(res.locals.chatSession._id);
|
||||||
? req.params.id[0]
|
|
||||||
: req.params.id;
|
|
||||||
if (!id) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Session ID is required",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ChatSessionService.delete(id);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Chat session deleted",
|
message: "Chat session deleted",
|
||||||
@ -286,20 +254,11 @@ class ChatSessionController extends DtpController {
|
|||||||
* GET /api/v1/chat-sessions/:id/turns
|
* GET /api/v1/chat-sessions/:id/turns
|
||||||
* Get all turns for a chat session.
|
* Get all turns for a chat session.
|
||||||
*/
|
*/
|
||||||
private async getSessionTurns(req: Request, res: Response): Promise<void> {
|
private async getSessionTurns(_req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id)
|
const turns = await ChatSessionService.getTurns(
|
||||||
? req.params.id[0]
|
res.locals.chatSession._id,
|
||||||
: req.params.id;
|
);
|
||||||
if (!id) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "Session ID is required",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const turns = await ChatSessionService.getTurns(id);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -9,11 +9,42 @@ import { DtpController } from "../../lib/controller.ts";
|
|||||||
|
|
||||||
import DroneService from "../../services/drone.ts";
|
import DroneService from "../../services/drone.ts";
|
||||||
import UserService from "../../services/user.ts";
|
import UserService from "../../services/user.ts";
|
||||||
|
import { ChatSessionService } from "../../services/index.js";
|
||||||
|
|
||||||
export interface PopulateOptions {
|
export interface PopulateOptions {
|
||||||
requireObject?: boolean;
|
requireObject?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function populateChatSessionById(
|
||||||
|
controller: DtpController,
|
||||||
|
options?: PopulateOptions,
|
||||||
|
): RequestHandler {
|
||||||
|
options = Object.assign({ requireObject: true }, options);
|
||||||
|
return async function (
|
||||||
|
_req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
sessionId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
assert(sessionId, "ChatSession ID is required");
|
||||||
|
try {
|
||||||
|
res.locals.chatSession = await ChatSessionService.getById(sessionId);
|
||||||
|
if (options.requireObject && !res.locals.chatSession) {
|
||||||
|
const error = new Error("ChatSession not found");
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
controller.log.error("failed to populate ChatSession by ID", {
|
||||||
|
sessionId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function populateUserById(
|
export function populateUserById(
|
||||||
controller: DtpController,
|
controller: DtpController,
|
||||||
options?: PopulateOptions,
|
options?: PopulateOptions,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { GadgetId } from "@gadget/api";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export enum ApiClientStatus {
|
export enum ApiClientStatus {
|
||||||
@ -13,6 +13,18 @@ export enum ApiClientStatus {
|
|||||||
Archived = "archived",
|
Archived = "archived",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An API client is either machine-to-machine (no User), or a User access client
|
||||||
|
* that can call services on the User's scoped behalf. When a request presents
|
||||||
|
* an ApiClient token, we populate req.user with that User account. The
|
||||||
|
* remainder of the request proceeds with that User's authentication and
|
||||||
|
* authorization.
|
||||||
|
*
|
||||||
|
* An example of non-User communication is a gadget-drone performing an initial
|
||||||
|
* Platform registration. It presents the User's credentials to authenticate the
|
||||||
|
* request, and we verify the API key to authenticate that client before
|
||||||
|
* trusting the credentials presented.
|
||||||
|
*/
|
||||||
export interface IApiClient {
|
export interface IApiClient {
|
||||||
_id: GadgetId;
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@ -21,6 +33,7 @@ export interface IApiClient {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
|
user?: IUser | GadgetId;
|
||||||
}
|
}
|
||||||
const ApiClientSchema = new Schema<IApiClient>({
|
const ApiClientSchema = new Schema<IApiClient>({
|
||||||
_id: { type: String, default: () => nanoid() },
|
_id: { type: String, default: () => nanoid() },
|
||||||
@ -36,6 +49,7 @@ const ApiClientSchema = new Schema<IApiClient>({
|
|||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
description: { type: String },
|
description: { type: String },
|
||||||
secret: { type: String, required: true },
|
secret: { type: String, required: true },
|
||||||
|
user: { type: String, ref: "User", index: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ApiClient = model<IApiClient>("ApiClient", ApiClientSchema);
|
export const ApiClient = model<IApiClient>("ApiClient", ApiClientSchema);
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
// import env, { getCountryName } from "../config/env.js";
|
// import env, { getCountryName } from "../config/env.js";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
|
||||||
import { Request } from "express";
|
import { NextFunction, Request, RequestHandler, Response } from "express";
|
||||||
|
|
||||||
import { filterText } from "dtp-cleantext";
|
import { filterText } from "dtp-cleantext";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
@ -18,8 +18,16 @@ import ApiClientLog, { IApiClientLog } from "../models/api-client-log.js";
|
|||||||
|
|
||||||
import { DtpService } from "../lib/service.js";
|
import { DtpService } from "../lib/service.js";
|
||||||
import { GadgetId } from "@gadget/api";
|
import { GadgetId } from "@gadget/api";
|
||||||
|
import { PopulateOptions } from "mongoose";
|
||||||
|
|
||||||
class ApiClientService extends DtpService {
|
class ApiClientService extends DtpService {
|
||||||
|
private populateApiClient: PopulateOptions[] = [
|
||||||
|
{
|
||||||
|
path: "user",
|
||||||
|
select: "-passwordSalt -password",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return "ApiClientService";
|
return "ApiClientService";
|
||||||
}
|
}
|
||||||
@ -35,12 +43,45 @@ class ApiClientService extends DtpService {
|
|||||||
|
|
||||||
async stop(): Promise<void> {}
|
async stop(): Promise<void> {}
|
||||||
|
|
||||||
|
middleware(): RequestHandler {
|
||||||
|
return async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const apiClientId = req.header("X-Gadget-Key") as string;
|
||||||
|
if (!apiClientId) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiClient = await this.getById(apiClientId);
|
||||||
|
if (!apiClient) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiClient.user && !req.user) {
|
||||||
|
req.user = apiClient.user;
|
||||||
|
res.locals.user = apiClient.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.logRequest(apiClient, req);
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("failed to process ApiClient request", { error });
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async create(definition: Partial<IApiClient>): Promise<IApiClient> {
|
async create(definition: Partial<IApiClient>): Promise<IApiClient> {
|
||||||
const NOW = new Date();
|
const NOW = new Date();
|
||||||
const apiClient = new ApiClient();
|
const apiClient = new ApiClient();
|
||||||
apiClient.createdAt = NOW;
|
apiClient.createdAt = NOW;
|
||||||
apiClient.updatedAt = NOW;
|
apiClient.updatedAt = NOW;
|
||||||
apiClient.status = ApiClientStatus.Active;
|
apiClient.status = ApiClientStatus.Active;
|
||||||
|
apiClient.user = definition.user;
|
||||||
|
|
||||||
assert(definition.name, "ApiClient name is required");
|
assert(definition.name, "ApiClient name is required");
|
||||||
apiClient.name = filterText(definition.name);
|
apiClient.name = filterText(definition.name);
|
||||||
@ -77,7 +118,9 @@ class ApiClientService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getById(clientId: GadgetId): Promise<IApiClient | null> {
|
async getById(clientId: GadgetId): Promise<IApiClient | null> {
|
||||||
const client = await ApiClient.findOne({ _id: clientId });
|
const client = await ApiClient.findOne({ _id: clientId })
|
||||||
|
.populate(this.populateApiClient)
|
||||||
|
.lean();
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
|
import env from "../config/env.js";
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IChatSession,
|
IChatSession,
|
||||||
ChatSessionMode,
|
ChatSessionMode,
|
||||||
@ -47,7 +52,7 @@ class ChatSessionService extends DtpService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "provider",
|
path: "provider",
|
||||||
select: "-models",
|
select: "-models +apiKey",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -234,6 +239,8 @@ class ChatSessionService extends DtpService {
|
|||||||
const user: IUser = session.user as IUser;
|
const user: IUser = session.user as IUser;
|
||||||
const project: IProject = session.project as IProject;
|
const project: IProject = session.project as IProject;
|
||||||
|
|
||||||
|
const systemPrompt = await this.buildSystemPrompt(session);
|
||||||
|
|
||||||
let turn = new ChatTurn({
|
let turn = new ChatTurn({
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
user: user._id,
|
user: user._id,
|
||||||
@ -244,8 +251,8 @@ class ChatSessionService extends DtpService {
|
|||||||
mode: session.mode,
|
mode: session.mode,
|
||||||
status: ChatTurnStatus.Processing,
|
status: ChatTurnStatus.Processing,
|
||||||
prompts: {
|
prompts: {
|
||||||
|
system: systemPrompt,
|
||||||
user: prompt,
|
user: prompt,
|
||||||
system: undefined,
|
|
||||||
},
|
},
|
||||||
toolCalls: [],
|
toolCalls: [],
|
||||||
subagents: [],
|
subagents: [],
|
||||||
@ -265,6 +272,40 @@ class ChatSessionService extends DtpService {
|
|||||||
return turn;
|
return turn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async buildSystemPrompt(session: IChatSession): Promise<string> {
|
||||||
|
const commonDir = path.join(env.installDir, "data", "prompts", "common");
|
||||||
|
const promptsDir = path.join(
|
||||||
|
env.installDir,
|
||||||
|
"data",
|
||||||
|
"prompts",
|
||||||
|
"agent",
|
||||||
|
session.mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
const common = {
|
||||||
|
scopeBlock: await fs.promises.readFile(
|
||||||
|
path.join(commonDir, "scope-block.md"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
subagentsBlock: await fs.promises.readFile(
|
||||||
|
path.join(commonDir, "subagents.md"),
|
||||||
|
"utf-8",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateFilename = path.join(promptsDir, "system.md");
|
||||||
|
const promptTemplate = await fs.promises.readFile(
|
||||||
|
templateFilename,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
let prompt = promptTemplate
|
||||||
|
.replace("{{scope_block}}", common.scopeBlock)
|
||||||
|
.replace("{{subagent_section}}", common.subagentsBlock);
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all turns for a chat session.
|
* Gets all turns for a chat session.
|
||||||
*/
|
*/
|
||||||
@ -272,10 +313,9 @@ class ChatSessionService extends DtpService {
|
|||||||
const turns = await ChatTurn.find({ session: chatSessionId })
|
const turns = await ChatTurn.find({ session: chatSessionId })
|
||||||
.populate("user", "-passwordSalt -password")
|
.populate("user", "-passwordSalt -password")
|
||||||
.populate("project")
|
.populate("project")
|
||||||
.populate("provider")
|
.populate("provider", "-models")
|
||||||
.sort({ createdAt: 1 })
|
.sort({ createdAt: 1 })
|
||||||
.lean();
|
.lean();
|
||||||
|
|
||||||
return turns;
|
return turns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import { HomeController } from "./controllers/home.js";
|
|||||||
import { UserController } from "./controllers/user.js";
|
import { UserController } from "./controllers/user.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ApiClientService,
|
||||||
SessionService,
|
SessionService,
|
||||||
SocketService,
|
SocketService,
|
||||||
startServices,
|
startServices,
|
||||||
@ -198,7 +199,9 @@ class DtpWebAppServer implements DtpComponent {
|
|||||||
},
|
},
|
||||||
store,
|
store,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.app.use(session(sessionConfig));
|
this.app.use(session(sessionConfig));
|
||||||
|
this.app.use(ApiClientService.middleware());
|
||||||
this.app.use(this.restoreUserSession.bind(this));
|
this.app.use(this.restoreUserSession.bind(this));
|
||||||
|
|
||||||
this.app.use("/", await this.createAppRouter());
|
this.app.use("/", await this.createAppRouter());
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import "./lib/db.js";
|
|||||||
* Models
|
* Models
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ApiClient, { ApiClientStatus } from "./models/api-client.js";
|
import ApiClient, { ApiClientStatus, IApiClient } from "./models/api-client.js";
|
||||||
import User from "./models/user.js";
|
import User from "./models/user.js";
|
||||||
import AiProvider from "./models/ai-provider.js";
|
import AiProvider from "./models/ai-provider.js";
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ import {
|
|||||||
import { createAiApi, type IAiLogger } from "@gadget/ai";
|
import { createAiApi, type IAiLogger } from "@gadget/ai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
IUser,
|
||||||
type IAiModel,
|
type IAiModel,
|
||||||
type IAiModelCapabilities,
|
type IAiModelCapabilities,
|
||||||
type IAiModelSettings,
|
type IAiModelSettings,
|
||||||
@ -157,29 +158,49 @@ class DtpWebCli extends DtpProcess {
|
|||||||
const name = argv.shift();
|
const name = argv.shift();
|
||||||
const description = argv.shift();
|
const description = argv.shift();
|
||||||
|
|
||||||
const client = await ApiClientService.create({
|
const email = argv.shift();
|
||||||
|
let user;
|
||||||
|
if (email) {
|
||||||
|
user = await UserService.getByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("user not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition: Partial<IApiClient> = {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
});
|
};
|
||||||
this.log.info("api client added", {
|
if (user) {
|
||||||
client: {
|
definition.user = user._id;
|
||||||
_id: client._id,
|
}
|
||||||
secret: client.secret,
|
|
||||||
name: client.name,
|
const client = await ApiClientService.create(definition);
|
||||||
},
|
this.printApiClientList([client]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onApiClientList(_argv: string[]): Promise<void> {
|
async onApiClientList(_argv: string[]): Promise<void> {
|
||||||
const clients = await ApiClient.find({ status: ApiClientStatus.Active })
|
const clients: IApiClient[] = await ApiClient.find({
|
||||||
|
status: ApiClientStatus.Active,
|
||||||
|
})
|
||||||
.sort({ name: 1 })
|
.sort({ name: 1 })
|
||||||
|
.populate([{ path: "user", select: "-passwordSalt -password" }])
|
||||||
.lean();
|
.lean();
|
||||||
|
this.printApiClientList(clients);
|
||||||
|
}
|
||||||
|
|
||||||
|
printApiClientList(clients: IApiClient[]) {
|
||||||
console.log("Name".padEnd(20), "Client ID".padEnd(24), "Secret");
|
console.log("Name".padEnd(20), "Client ID".padEnd(24), "Secret");
|
||||||
console.log(
|
console.log(
|
||||||
"--------------------------------------------------------------------------------",
|
"--------------------------------------------------------------------------------",
|
||||||
);
|
);
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
console.log(client.name.padEnd(20), client._id.toString(), client.secret);
|
let log = `${client.name.padEnd(20)} ${client._id.toString()} ${client.secret}`;
|
||||||
|
if (client.user) {
|
||||||
|
const user = client.user as IUser;
|
||||||
|
log += ` ${user._id} ${user.email}`;
|
||||||
|
}
|
||||||
|
console.log(log);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +244,8 @@ class DtpWebCli extends DtpProcess {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case "add":
|
case "add":
|
||||||
return this.onUserAdd(argv);
|
return this.onUserAdd(argv);
|
||||||
|
case "view":
|
||||||
|
return this.onUserView(argv);
|
||||||
case "password":
|
case "password":
|
||||||
return this.onUserPassword(argv);
|
return this.onUserPassword(argv);
|
||||||
case "remove":
|
case "remove":
|
||||||
@ -253,6 +276,20 @@ class DtpWebCli extends DtpProcess {
|
|||||||
this.log.info(`user created: id:${user._id}, email:${user.email}`);
|
this.log.info(`user created: id:${user._id}, email:${user.email}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onUserView(argv: string[]): Promise<void> {
|
||||||
|
const email = argv.shift();
|
||||||
|
if (!email) {
|
||||||
|
throw new Error("must specify email address");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await UserService.getByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("user not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info("user account", user);
|
||||||
|
}
|
||||||
|
|
||||||
async onUserRemove(argv: string[]): Promise<void> {
|
async onUserRemove(argv: string[]): Promise<void> {
|
||||||
let email = argv.shift();
|
let email = argv.shift();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
|
|||||||
@ -442,9 +442,16 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
return cb(false, "this drone's workspace is not in Agent mode");
|
return cb(false, "this drone's workspace is not in Agent mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.socket) {
|
||||||
|
this.log.error("cannot process work order: no socket connection");
|
||||||
|
cb(false, "No socket connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = await PlatformService.getChatSessionContext(session);
|
||||||
const order: IAgentWorkOrder = {
|
const order: IAgentWorkOrder = {
|
||||||
createdAt: turn.createdAt,
|
createdAt: turn.createdAt,
|
||||||
context: [],
|
context: context.data,
|
||||||
turn,
|
turn,
|
||||||
};
|
};
|
||||||
this.log.info("processWorkOrder received", {
|
this.log.info("processWorkOrder received", {
|
||||||
@ -454,12 +461,6 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
turn: { _id: turn._id, mode: turn.mode, userPrompt: turn.prompts.user },
|
turn: { _id: turn._id, mode: turn.mode, userPrompt: turn.prompts.user },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.socket) {
|
|
||||||
this.log.error("cannot process work order: no socket connection");
|
|
||||||
cb(false, "No socket connection");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write work order cache BEFORE processing (for crash recovery)
|
// Write work order cache BEFORE processing (for crash recovery)
|
||||||
try {
|
try {
|
||||||
await WorkspaceService.writeWorkOrderCache(turn);
|
await WorkspaceService.writeWorkOrderCache(turn);
|
||||||
|
|||||||
@ -9,7 +9,14 @@ import path from "node:path";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
import { GadgetService } from "../lib/service.ts";
|
import { GadgetService } from "../lib/service.ts";
|
||||||
import { DroneStatus, IDroneRegistration, IUser, Types } from "@gadget/api";
|
import {
|
||||||
|
DroneStatus,
|
||||||
|
IChatSession,
|
||||||
|
IChatTurn,
|
||||||
|
IDroneRegistration,
|
||||||
|
IUser,
|
||||||
|
Types,
|
||||||
|
} from "@gadget/api";
|
||||||
|
|
||||||
interface PlatformApiResponse {
|
interface PlatformApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -20,6 +27,10 @@ interface PlatformRegistrationResponse extends PlatformApiResponse {
|
|||||||
data: IDroneRegistration;
|
data: IDroneRegistration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChatSessionContextResponse extends PlatformApiResponse {
|
||||||
|
data: IChatTurn[];
|
||||||
|
}
|
||||||
|
|
||||||
class PlatformService extends GadgetService {
|
class PlatformService extends GadgetService {
|
||||||
registration: IDroneRegistration | undefined;
|
registration: IDroneRegistration | undefined;
|
||||||
|
|
||||||
@ -157,6 +168,37 @@ class PlatformService extends GadgetService {
|
|||||||
this.log.info("drone status updated on platform", { status });
|
this.log.info("drone status updated on platform", { status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChatSessionContext(
|
||||||
|
session: IChatSession,
|
||||||
|
): Promise<ChatSessionContextResponse> {
|
||||||
|
assert(
|
||||||
|
this.registration,
|
||||||
|
"must register with platform before setting status",
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = this.getApiUrl(`/chat-sessions/${session._id}/turns`);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-Gadget-Key": env.platform.apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = (await response.json()) as ChatSessionContextResponse;
|
||||||
|
if (!json.success) {
|
||||||
|
const error = new Error("failed to retrieve chat session context");
|
||||||
|
error.name = "PlatformError";
|
||||||
|
error.statusCode = response.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info("chat session context received", {
|
||||||
|
turnCount: json.data.length,
|
||||||
|
});
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
getApiUrl(url: string): string {
|
getApiUrl(url: string): string {
|
||||||
return `${env.platform.baseUrl}/api/v1${url}`;
|
return `${env.platform.baseUrl}/api/v1${url}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user