|
|
@@ -1,92 +0,0 @@
|
|
|
-import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
|
-import axios from 'axios';
|
|
|
-import { MongoCoreService } from 'src/mongo/mongo-core.service';
|
|
|
-import { FFBProductionRepository } from 'src/mongo/mongo-ffb-production.repository';
|
|
|
-import { FFBProduction } from './ffb-production.schema';
|
|
|
-
|
|
|
-@Injectable()
|
|
|
-export class FFBVectorService implements OnModuleInit {
|
|
|
- private repo: FFBProductionRepository;
|
|
|
- private readonly VECTOR_DIM = parseInt(process.env.VECTOR_DIM || '1536'); // OpenAI default
|
|
|
- private accessToken: string;
|
|
|
-
|
|
|
- constructor(private readonly mongoCore: MongoCoreService) { }
|
|
|
-
|
|
|
- async onModuleInit() {
|
|
|
- // Initialize Mongo repository
|
|
|
- const db = await this.mongoCore.getDb();
|
|
|
- this.repo = new FFBProductionRepository(db);
|
|
|
- await this.repo.init();
|
|
|
-
|
|
|
- // Load OpenAI API key
|
|
|
- this.accessToken = process.env.OPENAI_API_KEY as unknown as string;
|
|
|
- if (!this.accessToken) {
|
|
|
- throw new Error('❌ Missing OPENAI_API_KEY in environment.');
|
|
|
- }
|
|
|
-
|
|
|
- console.log('✅ OpenAI embedding service ready. Repository initialized.');
|
|
|
- }
|
|
|
-
|
|
|
- /** Convert a record to a text string suitable for embedding */
|
|
|
- private recordToText(record: FFBProduction): string {
|
|
|
- return `Production on ${new Date(record.productionDate).toISOString()} at ${record.site} in ${record.phase} ${record.block} produced ${record.quantity} ${record.quantityUom} with a total weight of ${record.weight} ${record.weightUom}.`;
|
|
|
- }
|
|
|
-
|
|
|
- /** Generate embedding via OpenAI */
|
|
|
- private async embedText(text: string): Promise<number[]> {
|
|
|
- const model = process.env.EMBEDDING_MODEL || 'text-embedding-3-small';
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await axios.post(
|
|
|
- 'https://api.openai.com/v1/embeddings',
|
|
|
- { model, input: text },
|
|
|
- {
|
|
|
- headers: {
|
|
|
- Authorization: `Bearer ${this.accessToken}`,
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- },
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- const embedding = response.data?.data?.[0]?.embedding;
|
|
|
-
|
|
|
- if (!embedding || !Array.isArray(embedding)) {
|
|
|
- throw new Error(`Invalid embedding returned: ${JSON.stringify(response.data)}`);
|
|
|
- }
|
|
|
-
|
|
|
- if (embedding.length !== this.VECTOR_DIM) {
|
|
|
- console.warn(
|
|
|
- `⚠️ Warning: embedding dimension mismatch. Expected ${this.VECTOR_DIM}, got ${embedding.length}`
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- return embedding;
|
|
|
- } catch (err: any) {
|
|
|
- console.error('❌ Failed to generate OpenAI embedding:', err.response?.data || err.message);
|
|
|
- throw err;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /** Insert a single record with embedding vector */
|
|
|
- async insertWithVector(record: FFBProduction) {
|
|
|
- const text = this.recordToText(record);
|
|
|
- const vector = await this.embedText(text);
|
|
|
-
|
|
|
- const data: FFBProduction & { vector: number[] } = { ...record, vector };
|
|
|
- return this.repo.create(data);
|
|
|
- }
|
|
|
-
|
|
|
- /** Search for top-k similar records using a text query */
|
|
|
- async vectorSearch(query: string, k = 5) {
|
|
|
- if (!query) throw new Error('Query string cannot be empty');
|
|
|
-
|
|
|
- const vector = await this.embedText(query);
|
|
|
- const results = await this.repo.vectorSearch(vector, k, 50);
|
|
|
-
|
|
|
- return results.map((r) => ({
|
|
|
- ...r,
|
|
|
- _id: r._id.toString(),
|
|
|
- score: r.score,
|
|
|
- }));
|
|
|
- }
|
|
|
-}
|