|
@@ -4,7 +4,8 @@ import { FFBVectorService } from './ffb-vector.service';
|
|
|
import { z } from "zod";
|
|
import { z } from "zod";
|
|
|
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";
|
|
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";
|
|
|
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
|
|
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
|
|
|
-
|
|
|
|
|
|
|
+import { forwardRef, Inject } from '@nestjs/common';
|
|
|
|
|
+import { FFBGateway } from '../ffb.gateway';
|
|
|
// State Definition using Annotation
|
|
// State Definition using Annotation
|
|
|
const AgentState = Annotation.Root({
|
|
const AgentState = Annotation.Root({
|
|
|
messages: Annotation<BaseMessage[]>({
|
|
messages: Annotation<BaseMessage[]>({
|
|
@@ -25,18 +26,22 @@ const AgentState = Annotation.Root({
|
|
|
}),
|
|
}),
|
|
|
finalResponse: Annotation<string>({
|
|
finalResponse: Annotation<string>({
|
|
|
reducer: (x, y) => y ?? x,
|
|
reducer: (x, y) => y ?? x,
|
|
|
|
|
+ }),
|
|
|
|
|
+ socketId: Annotation<string>({
|
|
|
|
|
+ reducer: (x, y) => y ?? x,
|
|
|
|
|
+ default: () => "default",
|
|
|
})
|
|
})
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-import { FFBGateway } from '../ffb.gateway';
|
|
|
|
|
-
|
|
|
|
|
@Injectable()
|
|
@Injectable()
|
|
|
export class FFBLangChainService {
|
|
export class FFBLangChainService {
|
|
|
private model: ChatOpenAI;
|
|
private model: ChatOpenAI;
|
|
|
private graph: any;
|
|
private graph: any;
|
|
|
|
|
+ private sessions: Map<string, BaseMessage[]> = new Map();
|
|
|
|
|
|
|
|
constructor(
|
|
constructor(
|
|
|
private readonly vectorService: FFBVectorService,
|
|
private readonly vectorService: FFBVectorService,
|
|
|
|
|
+ @Inject(forwardRef(() => FFBGateway))
|
|
|
private readonly gateway: FFBGateway
|
|
private readonly gateway: FFBGateway
|
|
|
) {
|
|
) {
|
|
|
this.model = new ChatOpenAI({
|
|
this.model = new ChatOpenAI({
|
|
@@ -97,7 +102,7 @@ export class FFBLangChainService {
|
|
|
reasoning: z.string()
|
|
reasoning: z.string()
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- this.gateway.emitThought({
|
|
|
|
|
|
|
+ this.gateway.emitThought(state.socketId, {
|
|
|
node: 'router_node',
|
|
node: 'router_node',
|
|
|
status: 'processing',
|
|
status: 'processing',
|
|
|
message: 'Analyzing user intent...',
|
|
message: 'Analyzing user intent...',
|
|
@@ -126,7 +131,7 @@ User Input: "${lastMessage}"
|
|
|
const result = await structuredLlm.invoke(routerPrompt);
|
|
const result = await structuredLlm.invoke(routerPrompt);
|
|
|
|
|
|
|
|
// Merge extracted entities with existing store
|
|
// Merge extracted entities with existing store
|
|
|
- this.gateway.emitThought({
|
|
|
|
|
|
|
+ this.gateway.emitThought(state.socketId, {
|
|
|
node: 'router_node',
|
|
node: 'router_node',
|
|
|
status: 'completed',
|
|
status: 'completed',
|
|
|
result: result
|
|
result: result
|
|
@@ -134,14 +139,15 @@ User Input: "${lastMessage}"
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
activeIntent: result.intent as any,
|
|
activeIntent: result.intent as any,
|
|
|
- entityStore: result.entities || {}
|
|
|
|
|
|
|
+ entityStore: result.entities || {},
|
|
|
|
|
+ socketId: state.socketId
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private async clarifierNode(state: typeof AgentState.State): Promise<Partial<typeof AgentState.State>> {
|
|
private async clarifierNode(state: typeof AgentState.State): Promise<Partial<typeof AgentState.State>> {
|
|
|
const prompt = `User mentioned ${JSON.stringify(state.entityStore)}. Ask them to clarify what they want to know (e.g., total production, specific issues, etc.).`;
|
|
const prompt = `User mentioned ${JSON.stringify(state.entityStore)}. Ask them to clarify what they want to know (e.g., total production, specific issues, etc.).`;
|
|
|
|
|
|
|
|
- this.gateway.emitThought({
|
|
|
|
|
|
|
+ this.gateway.emitThought(state.socketId, {
|
|
|
node: 'clarifier_node',
|
|
node: 'clarifier_node',
|
|
|
status: 'processing',
|
|
status: 'processing',
|
|
|
message: 'Asking for clarification',
|
|
message: 'Asking for clarification',
|
|
@@ -174,7 +180,7 @@ User Input: "${lastMessage}"
|
|
|
|
|
|
|
|
const results = await this.vectorService.vectorSearch(lastMessage, 5, filter);
|
|
const results = await this.vectorService.vectorSearch(lastMessage, 5, filter);
|
|
|
|
|
|
|
|
- this.gateway.emitThought({
|
|
|
|
|
|
|
+ this.gateway.emitThought(state.socketId, {
|
|
|
node: 'vector_search_node',
|
|
node: 'vector_search_node',
|
|
|
status: 'completed',
|
|
status: 'completed',
|
|
|
query: lastMessage,
|
|
query: lastMessage,
|
|
@@ -233,7 +239,7 @@ User Input: "${lastMessage}"
|
|
|
|
|
|
|
|
const results = await this.vectorService.aggregate(pipeline);
|
|
const results = await this.vectorService.aggregate(pipeline);
|
|
|
|
|
|
|
|
- this.gateway.emitThought({
|
|
|
|
|
|
|
+ this.gateway.emitThought(state.socketId, {
|
|
|
node: 'aggregation_node',
|
|
node: 'aggregation_node',
|
|
|
status: 'completed',
|
|
status: 'completed',
|
|
|
pipeline: pipeline,
|
|
pipeline: pipeline,
|
|
@@ -257,7 +263,7 @@ User Input: "${lastMessage}"
|
|
|
Cite the source (e.g., "Based on aggregation results...").
|
|
Cite the source (e.g., "Based on aggregation results...").
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
- this.gateway.emitThought({
|
|
|
|
|
|
|
+ this.gateway.emitThought(state.socketId, {
|
|
|
node: 'synthesis_node',
|
|
node: 'synthesis_node',
|
|
|
status: 'processing',
|
|
status: 'processing',
|
|
|
message: 'Synthesizing final response',
|
|
message: 'Synthesizing final response',
|
|
@@ -272,16 +278,36 @@ User Input: "${lastMessage}"
|
|
|
|
|
|
|
|
// --- MAIN ENTRY POINT ---
|
|
// --- MAIN ENTRY POINT ---
|
|
|
|
|
|
|
|
- async chat(message: string): Promise<string> {
|
|
|
|
|
|
|
+ createSession(socketId: string) {
|
|
|
|
|
+ this.sessions.set(socketId, []);
|
|
|
|
|
+ console.log(`Session created for ${socketId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ deleteSession(socketId: string) {
|
|
|
|
|
+ this.sessions.delete(socketId);
|
|
|
|
|
+ console.log(`Session deleted for ${socketId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async chat(socketId: string, message: string): Promise<string> {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // Get history or init empty
|
|
|
|
|
+ const history = this.sessions.get(socketId) || [];
|
|
|
|
|
+
|
|
|
const inputs = {
|
|
const inputs = {
|
|
|
- messages: [new HumanMessage(message)],
|
|
|
|
|
- entityStore: {}
|
|
|
|
|
|
|
+ messages: [...history, new HumanMessage(message)],
|
|
|
|
|
+ entityStore: {},
|
|
|
|
|
+ socketId: socketId
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const result = await this.graph.invoke(inputs);
|
|
const result = await this.graph.invoke(inputs);
|
|
|
|
|
|
|
|
- const agentMessages = result.messages.filter((m: BaseMessage) => m._getType() === 'ai');
|
|
|
|
|
|
|
+ const allMessages = result.messages as BaseMessage[];
|
|
|
|
|
+
|
|
|
|
|
+ // Update history (keep all messages for context window? Or truncate?)
|
|
|
|
|
+ // For now, keep all. Memory optimization might be needed later.
|
|
|
|
|
+ this.sessions.set(socketId, allMessages);
|
|
|
|
|
+
|
|
|
|
|
+ const agentMessages = allMessages.filter((m: BaseMessage) => m._getType() === 'ai');
|
|
|
const lastResponse = agentMessages[agentMessages.length - 1];
|
|
const lastResponse = agentMessages[agentMessages.length - 1];
|
|
|
|
|
|
|
|
return lastResponse.content as string;
|
|
return lastResponse.content as string;
|