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
222 lines
7.4 KiB
TypeScript
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;
|
|
}
|