gadget/gadget-code/src/config/env.ts
Rob Colbert 56f35a2fdf feat: Qdrant semantic search over chat history
Adds vector-based semantic search across all chat sessions using Qdrant.
When a ChatTurn finishes, its content is chunked, embedded, and upserted
to a Qdrant collection. A search API and UI components enable searching
at user, project, and session scope.

Phase 1 — Configuration & Dependencies
- Add port/apiKey to GadgetCodeConfig.qdrant type
- Uncomment and update qdrant section in YAML config example
- Add qdrant config passthrough in env.ts
- Add @qdrant/js-client-rest dependency

Phase 2 — AI Embedding API (@gadget/ai)
- Add IAiEmbeddingResponse interface and abstract embeddings() to AiApi
- Implement embeddings() in OllamaAiApi (client.embeddings)
- Implement embeddings() in OpenAiApi (client.embeddings.create)
- Export IAiEmbeddingResponse from package index

Phase 3 — Backend Vector Store Service
- Create VectorStoreService (ingestTurn, search, removeTurnPoints)
- Hook fire-and-forget ingest after turn.save() in drone-session
- Register VectorStoreService in service startup/shutdown

Phase 4 — Backend Search API
- Create POST /api/v1/search controller with userId enforcement
- Batch-hydrate results from MongoDB (user, project, session, turn)
- Register search route in v1 API router

Phase 5 — Frontend Search Components
- SearchInput: debounced input with lucide-react icons
- ChatSearchResults: modal with score badges, metadata, loading states
- DroneSelectionModal: drone picker for sessions without a drone
- Add searchApi and ISearchResult to API client
- Add search to Home (global), ProjectManager (project), ChatSessionView (session)
- Add id=turn-{turnId} to ChatTurn for scroll targeting
- Scroll-to-turn from search result selection and router state
- Show DroneSelectionModal when no drone available
- Add Select Drone button in ChatSessionView sidebar
2026-05-19 14:28:30 -04:00

222 lines
7.4 KiB
TypeScript

// config/env.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { loadGadgetCodeConfig, resolvePath } from "@gadget/config";
import type PackageJson from "../../package.json";
import { GadgetLog, GadgetLogFile } from "@gadget/api";
const __dirname = dirname(fileURLToPath(import.meta.url));
// INSTALL_DIR: where the package is installed (for loading assets, etc.)
export const INSTALL_DIR = path.resolve(__dirname, "..", "..");
// Load YAML configuration
const yamlConfig = loadGadgetCodeConfig();
// Validate required fields
if (!yamlConfig.auth?.jwtSecret) {
throw new Error(
"Configuration error: auth.jwtSecret is required in gadget-code.yaml\n" +
"See documentation: ./docs/configuration.md",
);
}
if (!yamlConfig.auth?.passwordSalt) {
throw new Error(
"Configuration error: auth.passwordSalt is required in gadget-code.yaml\n" +
"See documentation: ./docs/configuration.md",
);
}
if (!yamlConfig.session?.secret) {
throw new Error(
"Configuration error: session.secret is required in gadget-code.yaml\n" +
"See documentation: ./docs/configuration.md",
);
}
async function readJsonFile<T>(filePath: string): Promise<T> {
const fs = await import("node:fs");
const file = await fs.promises.readFile(filePath);
return JSON.parse(file.toString("utf-8")) as T;
}
export interface ISiteDefinition {
company: string;
companyShort: string;
name: string;
shortName: string;
tagline: string;
slogan: string;
description: string;
domain: string;
domainKey: string;
host: string;
}
export default {
NODE_ENV: process.env.NODE_ENV,
timezone: yamlConfig.timezone || "America/New_York",
installDir: INSTALL_DIR,
pkg: await readJsonFile<typeof PackageJson>(
path.join(INSTALL_DIR, "package.json"),
),
site: {
company: yamlConfig.site?.company || "Robert Colbert",
companyShort: yamlConfig.site?.companyShort || "Colbert",
name: yamlConfig.site?.name || "Gadget Code",
shortName: yamlConfig.site?.shortName || "Gadget Code",
slogan:
yamlConfig.site?.slogan || "Self-hosted Agentic Engineering Platform",
description:
yamlConfig.site?.description ||
"Gadget Code - A self-hosted Agentic Engineering Platform (AEP).",
domain: yamlConfig.site?.domain || "code-dev.g4dge7.com",
domainKey: yamlConfig.site?.domainKey || "code-dev.g4dge7.com",
host: yamlConfig.site?.host || "code-dev.g4dge7.com",
},
ai: {
ollama: {
apiUrl: process.env.DTP_OLLAMA_API_URL || "http://localhost:11434",
apiKey: process.env.DTP_OLLAMA_API_KEY || "",
},
},
auth: {
jwtSecret: yamlConfig.auth.jwtSecret,
},
session: {
secret: yamlConfig.session.secret,
trustProxy:
process.env.NODE_ENV === "production" ||
yamlConfig.session?.trustProxy === true,
cookie: {
secure: yamlConfig.session?.cookie?.secure === true,
sameSite: yamlConfig.session?.cookie?.sameSite || false,
},
},
google: {
cse: {
apiKey: yamlConfig.google.cse.apiKey,
engineId: yamlConfig.google.cse.engineId,
},
},
mongodb: {
host: yamlConfig.mongodb?.host || "localhost",
database: yamlConfig.mongodb?.database || "",
},
qdrant: {
host: yamlConfig.qdrant?.host || "localhost",
port: yamlConfig.qdrant?.port || 6333,
apiKey: yamlConfig.qdrant?.apiKey,
collection: yamlConfig.qdrant?.collection || "gadget-chat-embeddings",
providerId: yamlConfig.qdrant?.providerId || "",
embeddingModel: yamlConfig.qdrant?.embeddingModel || "nomic-embed-text",
vectorSize: yamlConfig.qdrant?.vectorSize || 768,
},
redis: {
host: yamlConfig.redis?.host || "localhost",
port: yamlConfig.redis?.port || 6379,
password: yamlConfig.redis?.password,
keyPrefix: yamlConfig.redis?.keyPrefix || "dtp",
lazyConnect: yamlConfig.redis?.lazyConnect === true,
},
minio: {
endpoint: yamlConfig.minio?.endpoint || "localhost",
port: yamlConfig.minio?.port || 9080,
useSsl: yamlConfig.minio?.useSsl === true,
accessKey: yamlConfig.minio?.accessKey,
secretKey: yamlConfig.minio?.secretKey,
buckets: {
uploads: yamlConfig.minio?.buckets?.uploads || "dtp-uploads",
images: yamlConfig.minio?.buckets?.images || "dtp-images",
videos: yamlConfig.minio?.buckets?.videos || "dtp-videos",
audios: yamlConfig.minio?.buckets?.audios || "dtp-audios",
},
},
user: {
passwordSalt: yamlConfig.auth.passwordSalt,
},
https: {
enabled: yamlConfig.https?.enabled === true,
address: yamlConfig.https?.address || "127.0.0.1",
port: yamlConfig.https?.port || 3443,
backlog: yamlConfig.https?.backlog || 16,
keyFile: yamlConfig.https?.keyFile,
crtFile: yamlConfig.https?.crtFile,
uploadPath: yamlConfig.https?.uploadPath || "/tmp",
},
socket: {
maxHttpBufferSize: yamlConfig.socket?.maxHttpBufferSize || 1048576,
},
frontend: {
port: 5173,
},
email: {
enabled: yamlConfig.email?.enabled === true,
smtp: {
host: yamlConfig.email?.smtp?.host || "localhost",
port: yamlConfig.email?.smtp?.port || 465,
secure: yamlConfig.email?.smtp?.secure === true,
from:
yamlConfig.email?.smtp?.from ||
"Digital Telepresence Support <support@digitaltelepresence.com>",
user: yamlConfig.email?.smtp?.user,
password: yamlConfig.email?.smtp?.password,
pool: {
enabled: yamlConfig.email?.smtp?.pool?.enabled === true,
maxConnections: yamlConfig.email?.smtp?.pool?.maxConnections || 5,
maxMessages: yamlConfig.email?.smtp?.pool?.maxMessages || 100,
},
},
contact: {
to:
yamlConfig.email?.contact?.to ||
"DTP Support <support@digitaltelepresence.com>",
},
},
log: {
https: {
enabled: yamlConfig.logging?.https?.enabled === true || false,
name: yamlConfig.logging?.https?.name || "gadget-code-https.log",
path: yamlConfig.logging?.https?.path
? resolvePath(yamlConfig.logging.https.path)
: "/var/log/dtp",
format: yamlConfig.logging?.https?.format || "combined",
},
console: {
enabled: yamlConfig.logging?.console?.enabled === true,
},
file: {
enabled: yamlConfig.logging?.file?.enabled === true,
path: yamlConfig.logging?.file?.path
? resolvePath(yamlConfig.logging.file.path)
: path.join(INSTALL_DIR, "logs"),
name: yamlConfig.logging?.file?.name || "gadget-code",
maxWritesPerFile: yamlConfig.logging?.file?.maxWritesPerFile || 10000,
maxFiles: yamlConfig.logging?.file?.maxFiles || 10,
},
levels: {
debug: yamlConfig.logging?.levels?.debug === true,
info: yamlConfig.logging?.levels?.info === true,
warn: yamlConfig.logging?.levels?.warn === true,
},
},
};
// Configure GadgetLog for this package
GadgetLog.consoleEnabled = yamlConfig.logging?.console?.enabled === true;
if (yamlConfig.logging?.file?.enabled === true) {
const logFileOptions = {
basePath: yamlConfig.logging.file.path
? resolvePath(yamlConfig.logging.file.path)
: path.join(INSTALL_DIR, "logs"),
name: yamlConfig.logging.file.name || "gadget-code",
maxWritesPerFile: yamlConfig.logging.file.maxWritesPerFile || 10000,
maxFiles: yamlConfig.logging.file.maxFiles || 10,
};
const defaultLogFile = new GadgetLogFile(logFileOptions);
defaultLogFile.open();
GadgetLog.defaultFile = defaultLogFile;
}