|
@@ -1,17 +1,17 @@
|
|
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
|
-import axios from 'axios';
|
|
|
|
|
-import { GoogleAuth } from 'google-auth-library';
|
|
|
|
|
import { MongoCoreService } from 'src/mongo/mongo-core.service';
|
|
import { MongoCoreService } from 'src/mongo/mongo-core.service';
|
|
|
import { FFBProductionRepository } from 'src/mongo/mongo-ffb-production.repository';
|
|
import { FFBProductionRepository } from 'src/mongo/mongo-ffb-production.repository';
|
|
|
import { FFBProduction } from './ffb-production.schema';
|
|
import { FFBProduction } from './ffb-production.schema';
|
|
|
|
|
+import { GeminiEmbeddingService } from './gemini-embedding.service';
|
|
|
|
|
|
|
|
@Injectable()
|
|
@Injectable()
|
|
|
export class FFBVectorService implements OnModuleInit {
|
|
export class FFBVectorService implements OnModuleInit {
|
|
|
private repo: FFBProductionRepository;
|
|
private repo: FFBProductionRepository;
|
|
|
- private readonly VECTOR_DIM = parseInt(process.env.VECTOR_DIM || '3072'); // Gemini default
|
|
|
|
|
- private accessToken: string; // OAuth token
|
|
|
|
|
|
|
|
|
|
- constructor(private readonly mongoCore: MongoCoreService) { }
|
|
|
|
|
|
|
+ constructor(
|
|
|
|
|
+ private readonly mongoCore: MongoCoreService,
|
|
|
|
|
+ private readonly embeddingService: GeminiEmbeddingService
|
|
|
|
|
+ ) {}
|
|
|
|
|
|
|
|
async onModuleInit() {
|
|
async onModuleInit() {
|
|
|
// Initialize Mongo repository
|
|
// Initialize Mongo repository
|
|
@@ -19,19 +19,6 @@ export class FFBVectorService implements OnModuleInit {
|
|
|
this.repo = new FFBProductionRepository(db);
|
|
this.repo = new FFBProductionRepository(db);
|
|
|
await this.repo.init();
|
|
await this.repo.init();
|
|
|
|
|
|
|
|
- // Authenticate with Google service account
|
|
|
|
|
- const auth = new GoogleAuth({
|
|
|
|
|
- keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
|
|
|
- scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const client = await auth.getClient();
|
|
|
|
|
- const tokenResponse = await client.getAccessToken();
|
|
|
|
|
- if (!tokenResponse.token) {
|
|
|
|
|
- throw new Error('Failed to obtain OAuth 2 access token for Gemini embeddings');
|
|
|
|
|
- }
|
|
|
|
|
- this.accessToken = tokenResponse.token;
|
|
|
|
|
-
|
|
|
|
|
console.log('✅ Gemini embedding service ready. Repository initialized.');
|
|
console.log('✅ Gemini embedding service ready. Repository initialized.');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -40,56 +27,10 @@ export class FFBVectorService implements OnModuleInit {
|
|
|
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}.`;
|
|
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 Google Gemini */
|
|
|
|
|
- private async embedText(text: string): Promise<number[]> {
|
|
|
|
|
- const project = process.env.GOOGLE_PROJECT_ID!;
|
|
|
|
|
- const model = process.env.EMBEDDING_MODEL || 'gemini-embedding-001';
|
|
|
|
|
- const location = 'us-central1';
|
|
|
|
|
-
|
|
|
|
|
- const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${model}:predict`;
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const response = await axios.post(
|
|
|
|
|
- url,
|
|
|
|
|
- {
|
|
|
|
|
- instances: [{ content: text }],
|
|
|
|
|
- parameters: { autoTruncate: true, outputDimensionality: this.VECTOR_DIM },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- headers: {
|
|
|
|
|
- Authorization: `Bearer ${this.accessToken}`,
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- const predictions = response.data?.predictions;
|
|
|
|
|
- if (!predictions || predictions.length === 0) {
|
|
|
|
|
- throw new Error(`No predictions returned from Gemini API: ${JSON.stringify(response.data)}`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const embedding = predictions[0]?.embeddings?.values;
|
|
|
|
|
- if (!embedding || !Array.isArray(embedding)) {
|
|
|
|
|
- throw new Error(`Invalid embedding format returned: ${JSON.stringify(predictions[0])}`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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 embedding:', err.response?.data || err.message);
|
|
|
|
|
- throw err;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/** Insert a single record with embedding vector */
|
|
/** Insert a single record with embedding vector */
|
|
|
async insertWithVector(record: FFBProduction) {
|
|
async insertWithVector(record: FFBProduction) {
|
|
|
const text = this.recordToText(record);
|
|
const text = this.recordToText(record);
|
|
|
- const vector = await this.embedText(text);
|
|
|
|
|
|
|
+ const vector = await this.embeddingService.embedText(text);
|
|
|
|
|
|
|
|
const data: FFBProduction & { vector: number[] } = { ...record, vector };
|
|
const data: FFBProduction & { vector: number[] } = { ...record, vector };
|
|
|
return this.repo.create(data);
|
|
return this.repo.create(data);
|
|
@@ -99,8 +40,9 @@ export class FFBVectorService implements OnModuleInit {
|
|
|
async vectorSearch(query: string, k = 5) {
|
|
async vectorSearch(query: string, k = 5) {
|
|
|
if (!query) throw new Error('Query string cannot be empty');
|
|
if (!query) throw new Error('Query string cannot be empty');
|
|
|
|
|
|
|
|
- const vector = await this.embedText(query);
|
|
|
|
|
|
|
+ const vector = await this.embeddingService.embedText(query);
|
|
|
const results = await this.repo.vectorSearch(vector, k, 50);
|
|
const results = await this.repo.vectorSearch(vector, k, 50);
|
|
|
|
|
+
|
|
|
return results.map((r) => ({
|
|
return results.map((r) => ({
|
|
|
...r,
|
|
...r,
|
|
|
_id: r._id.toString(),
|
|
_id: r._id.toString(),
|