Created
November 23, 2025 20:48
-
-
Save netgfx/47e51ba2ade83ce21689e89f4329d081 to your computer and use it in GitHub Desktop.
Hybrid System with Action Planning & Grounding Validation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // server.js - Hybrid System with Action Planning & Grounding Validation | |
| import express from "express"; | |
| import dotenv from "dotenv"; | |
| import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; | |
| import { StateGraph, START, END } from "@langchain/langgraph"; | |
| import axios from "axios"; | |
| dotenv.config(); | |
| const app = express(); | |
| app.use(express.json()); | |
| // --- CONFIGURATION --- | |
| const PORT = 3000; | |
| const PARALLEL_API_KEY = process.env.PARALLEL_API_KEY; | |
| const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY; | |
| // LLM for generating plans and answers | |
| const model = new ChatGoogleGenerativeAI({ | |
| model: "gemini-2.5-flash", | |
| temperature: 0.3, | |
| apiKey: GOOGLE_API_KEY, | |
| }); | |
| console.log("\n" + "=".repeat(70)); | |
| console.log("π HYBRID SYSTEM - Action Planning Architecture"); | |
| console.log("=".repeat(70)); | |
| console.log("Flow: Plan β Ground β Validate β Execute β Verify"); | |
| console.log("=".repeat(70) + "\n"); | |
| // --- STATE DEFINITION --- | |
| const graphState = { | |
| userQuery: null, | |
| // Planning Phase | |
| actionPlan: null, // LLM's proposed actions to answer query | |
| requiredFacts: null, // Facts that need grounding | |
| // Grounding Phase | |
| groundedFacts: null, // Facts verified via web search | |
| groundingIssues: null, // Facts that failed verification | |
| // Validation Phase | |
| planValid: false, // Is the plan grounded in reality? | |
| corrections: null, // Suggested fixes for invalid plan | |
| // Execution Phase | |
| finalAnswer: null, // Answer after applying corrections | |
| confidence: 0, // Deterministic confidence score | |
| // Output | |
| finalJson: null, | |
| sources: [], | |
| }; | |
| // ======================================== | |
| // NODE 1: ACTION PLANNER | |
| // ======================================== | |
| async function planActions(state) { | |
| console.log(`\n[${"1".padStart(2, "0")}] π ACTION PLANNER`); | |
| console.log(`Query: "${state.userQuery}"`); | |
| const prompt = ` | |
| You are a query analyzer. Break down how you would answer this query into verifiable steps. | |
| USER QUERY: "${state.userQuery}" | |
| TASK: Output a JSON plan with: | |
| 1. "actions": Step-by-step actions you'll take | |
| 2. "required_facts": Facts that MUST be verified via external sources | |
| 3. "reasoning": Why these facts need verification | |
| EXAMPLES: | |
| Query: "What is Dyno?" | |
| { | |
| "actions": [ | |
| "Determine which 'Dyno' entity the user is asking about", | |
| "Check if it's Heroku Dyno (container technology)", | |
| "Check if it's Dyno Nobel (explosives company)", | |
| "Check if it's Dynojet (dynamometer)" | |
| ], | |
| "required_facts": [ | |
| "Existence of Heroku Dyno and its purpose", | |
| "Existence of Dyno Nobel and its industry", | |
| "Existence of Dynojet and its product type" | |
| ], | |
| "reasoning": "Ambiguous term - multiple entities exist with this name. Must verify which ones are real and what they do." | |
| } | |
| Query: "What is the latest version of React?" | |
| { | |
| "actions": [ | |
| "Search for React's current stable release", | |
| "Verify the version number from official sources", | |
| "Confirm release date if possible" | |
| ], | |
| "required_facts": [ | |
| "Current React version number (as of November 2025)", | |
| "Source: Official React documentation or npm registry" | |
| ], | |
| "reasoning": "Version information changes frequently and my training data may be outdated. Must verify from current sources." | |
| } | |
| Query: "What is the Flux Capacitor pattern in microservices?" | |
| { | |
| "actions": [ | |
| "Search for 'Flux Capacitor pattern' in microservices literature", | |
| "Check if this is a recognized architectural pattern", | |
| "Verify existence in authoritative sources" | |
| ], | |
| "required_facts": [ | |
| "Existence of Flux Capacitor pattern in software architecture", | |
| "Documentation or papers describing this pattern" | |
| ], | |
| "reasoning": "Unfamiliar pattern name - could be a hallucination or fictional reference. Must verify it exists in technical literature." | |
| } | |
| Now analyze this query: | |
| USER QUERY: "${state.userQuery}" | |
| Return ONLY valid JSON (no markdown): | |
| `; | |
| const response = await model.invoke(prompt); | |
| const planJson = response.content.replace(/```json|```/g, "").trim(); | |
| const plan = JSON.parse(planJson); | |
| console.log(`Actions planned: ${plan.actions.length}`); | |
| plan.actions.forEach((action, i) => { | |
| console.log(` ${i + 1}. ${action}`); | |
| }); | |
| console.log(`\nFacts requiring verification: ${plan.required_facts.length}`); | |
| plan.required_facts.forEach((fact, i) => { | |
| console.log(` ${i + 1}. ${fact}`); | |
| }); | |
| return { | |
| actionPlan: plan, | |
| requiredFacts: plan.required_facts, | |
| }; | |
| } | |
| // ======================================== | |
| // NODE 2: FACT GROUNDER | |
| // ======================================== | |
| async function groundFacts(state) { | |
| console.log(`\n[${"2".padStart(2, "0")}] π FACT GROUNDER`); | |
| console.log( | |
| `Verifying ${state.requiredFacts.length} facts via web search...` | |
| ); | |
| const grounded = []; | |
| const issues = []; | |
| for (const fact of state.requiredFacts) { | |
| console.log(`\n Searching: "${fact}"`); | |
| try { | |
| const response = await axios.post( | |
| "https://api.parallel.ai/v1beta/search", | |
| { | |
| mode: "one-shot", | |
| objective: fact, | |
| search_queries: [fact], | |
| max_results: 3, | |
| }, | |
| { | |
| headers: { | |
| "x-api-key": PARALLEL_API_KEY, | |
| "Content-Type": "application/json", | |
| "parallel-beta": "search-extract-2025-10-10", | |
| }, | |
| timeout: 15000, | |
| } | |
| ); | |
| const results = response.data.results || []; | |
| if (results.length === 0) { | |
| console.log(` β NO RESULTS - likely does not exist`); | |
| issues.push({ | |
| fact, | |
| issue: "NOT_FOUND", | |
| evidence: "No web search results found for this claim", | |
| }); | |
| } else { | |
| const sources = results.map((r) => ({ | |
| title: r.title, | |
| url: r.url || r.link, | |
| content: r.excerpt || r.content || r.snippet || "(title only)", | |
| })); | |
| console.log(` β Found ${results.length} sources`); | |
| results.forEach((r) => console.log(` - ${r.title}`)); | |
| grounded.push({ | |
| fact, | |
| status: "VERIFIED", | |
| sources, | |
| }); | |
| } | |
| } catch (error) { | |
| console.log(` β οΈ ERROR: ${error.message}`); | |
| issues.push({ | |
| fact, | |
| issue: "SEARCH_FAILED", | |
| evidence: error.message, | |
| }); | |
| } | |
| } | |
| console.log(`\nGrounding Results:`); | |
| console.log(` β Verified: ${grounded.length}`); | |
| console.log(` β Issues: ${issues.length}`); | |
| return { | |
| groundedFacts: grounded, | |
| groundingIssues: issues, | |
| }; | |
| } | |
| // ======================================== | |
| // NODE 3: PLAN VALIDATOR (Deterministic!) | |
| // ======================================== | |
| async function validatePlan(state) { | |
| console.log(`\n[${"3".padStart(2, "0")}] β PLAN VALIDATOR (Deterministic)`); | |
| const { groundedFacts, groundingIssues } = state; | |
| // DETERMINISTIC VALIDATION RULES | |
| const totalFacts = groundedFacts.length + groundingIssues.length; | |
| const verifiedCount = groundedFacts.length; | |
| const verificationRate = verifiedCount / totalFacts; | |
| console.log(`Verification Rate: ${(verificationRate * 100).toFixed(1)}%`); | |
| let planValid = false; | |
| let confidence = 0; | |
| const corrections = []; | |
| // RULE 1: If >80% facts verified β VALID | |
| if (verificationRate >= 0.8) { | |
| planValid = true; | |
| confidence = 0.85 + (verificationRate - 0.8) * 0.5; // 0.85-0.95 | |
| console.log(`β VALID - High verification rate`); | |
| } | |
| // RULE 2: If 50-80% verified β PARTIAL (need corrections) | |
| else if (verificationRate >= 0.5) { | |
| planValid = false; | |
| confidence = 0.5 + (verificationRate - 0.5) * 0.5; // 0.5-0.65 | |
| console.log(`β οΈ PARTIAL - Some facts unverified, need corrections`); | |
| groundingIssues.forEach((issue) => { | |
| if (issue.issue === "NOT_FOUND") { | |
| corrections.push({ | |
| problem: `Fact not found: "${issue.fact}"`, | |
| recommendation: "Remove this claim or find alternative evidence", | |
| severity: "HIGH", | |
| }); | |
| } | |
| }); | |
| } | |
| // RULE 3: If <50% verified β INVALID | |
| else { | |
| planValid = false; | |
| confidence = 0.1 + verificationRate * 0.3; // 0.1-0.25 | |
| console.log(`β INVALID - Too many unverified facts`); | |
| corrections.push({ | |
| problem: `Only ${(verificationRate * 100).toFixed( | |
| 0 | |
| )}% of facts could be verified`, | |
| recommendation: | |
| "Query is likely based on hallucinated or outdated information. Cannot provide reliable answer.", | |
| severity: "CRITICAL", | |
| }); | |
| } | |
| // DETERMINISTIC CONFIDENCE CALCULATION | |
| // Based on: verification rate + source quality + source count | |
| const sourceQuality = groundedFacts.reduce((score, g) => { | |
| const hasAuthoritative = g.sources.some( | |
| (s) => | |
| s.url && | |
| (s.url.includes("wikipedia.org") || | |
| s.url.includes("github.com") || | |
| s.url.includes(".gov") || | |
| s.url.includes(".edu") || | |
| s.url.includes("npmjs.com") || | |
| s.url.includes("python.org")) | |
| ); | |
| return score + (hasAuthoritative ? 1 : 0); | |
| }, 0); | |
| const qualityBonus = (sourceQuality / groundedFacts.length) * 0.1; | |
| confidence = Math.min(0.95, confidence + qualityBonus); | |
| console.log(`Confidence Score: ${confidence.toFixed(2)} (deterministic)`); | |
| console.log(` - Verification rate: ${(verificationRate * 100).toFixed(0)}%`); | |
| console.log( | |
| ` - Authoritative sources: ${sourceQuality}/${groundedFacts.length}` | |
| ); | |
| if (corrections.length > 0) { | |
| console.log(`\nCorrections needed:`); | |
| corrections.forEach((c, i) => { | |
| console.log(` ${i + 1}. [${c.severity}] ${c.problem}`); | |
| console.log(` β ${c.recommendation}`); | |
| }); | |
| } | |
| return { | |
| planValid, | |
| confidence, | |
| corrections: corrections.length > 0 ? corrections : null, | |
| }; | |
| } | |
| // ======================================== | |
| // NODE 4: ANSWER EXECUTOR (with corrections) | |
| // ======================================== | |
| async function executeAnswer(state) { | |
| console.log(`\n[${"4".padStart(2, "0")}] π― ANSWER EXECUTOR`); | |
| if (!state.planValid && state.corrections) { | |
| const criticalIssues = state.corrections.filter( | |
| (c) => c.severity === "CRITICAL" | |
| ); | |
| if (criticalIssues.length > 0) { | |
| console.log( | |
| `β Cannot execute - critical issues prevent reliable answer` | |
| ); | |
| return { | |
| finalAnswer: null, | |
| finalJson: { | |
| entity_name: "Unknown", | |
| category: "UNKNOWN", | |
| creation_date: null, | |
| confidence_score: state.confidence, | |
| validation_status: "REJECTED", | |
| error: "Plan validation failed - facts could not be verified", | |
| issues: state.corrections, | |
| sources: [], | |
| }, | |
| }; | |
| } | |
| } | |
| // Build context from grounded facts | |
| const context = state.groundedFacts | |
| .map((g) => { | |
| const sourceText = g.sources | |
| .map((s) => `Title: ${s.title}\nURL: ${s.url}\nContent: ${s.content}`) | |
| .join("\n"); | |
| return `VERIFIED FACT: ${g.fact}\n\nEVIDENCE:\n${sourceText}`; | |
| }) | |
| .join("\n\n" + "=".repeat(50) + "\n\n"); | |
| const corrections = state.corrections || []; | |
| const correctionText = | |
| corrections.length > 0 | |
| ? `\n\nWARNINGS/CORRECTIONS:\n${corrections | |
| .map((c) => `- ${c.problem}\n Fix: ${c.recommendation}`) | |
| .join("\n")}` | |
| : ""; | |
| const prompt = ` | |
| You are answering a user query using ONLY verified facts from web sources. | |
| USER QUERY: "${state.userQuery}" | |
| VERIFIED FACTS (from web search): | |
| ${context}${correctionText} | |
| INSTRUCTIONS: | |
| - Base your answer ONLY on the verified facts above | |
| - DO NOT make claims that aren't supported by the evidence | |
| - If multiple entities match the query (e.g., "Dyno"), mention the most relevant one(s) | |
| - Be concise but accurate | |
| - If facts are limited, acknowledge uncertainty | |
| Provide a clear, factual answer (2-3 sentences): | |
| `; | |
| const response = await model.invoke(prompt); | |
| const answer = response.content.trim(); | |
| console.log(`β Generated answer: "${answer.substring(0, 100)}..."`); | |
| return { | |
| finalAnswer: answer, | |
| }; | |
| } | |
| // ======================================== | |
| // NODE 5: OUTPUT STRUCTURER | |
| // ======================================== | |
| async function structureOutput(state) { | |
| console.log(`\n[${"5".padStart(2, "0")}] π¦ OUTPUT STRUCTURER`); | |
| if (!state.finalAnswer) { | |
| console.log(`Using pre-built rejection response`); | |
| return {}; // Already set in executeAnswer | |
| } | |
| const allSources = state.groundedFacts | |
| .flatMap((g) => g.sources.map((s) => s.url)) | |
| .filter(Boolean); | |
| const prompt = ` | |
| Convert this answer into structured JSON. | |
| ANSWER: "${state.finalAnswer}" | |
| QUERY: "${state.userQuery}" | |
| CONFIDENCE: ${state.confidence} | |
| Extract: | |
| - entity_name: Main entity being discussed | |
| - category: SOFTWARE | HARDWARE | COMPANY | PERSON | CONCEPT | PLACE | OTHER | |
| - creation_date: YYYY-MM-DD if mentioned, else null | |
| - validation_status: Use "ACCEPTED" (plan was valid with minor/no corrections) or "CORRECTED" (plan had issues but was fixed) | |
| Return ONLY JSON (no markdown): | |
| { | |
| "entity_name": "...", | |
| "category": "...", | |
| "creation_date": "...", | |
| "confidence_score": ${state.confidence}, | |
| "validation_status": "...", | |
| "answer": "${state.finalAnswer.replace(/"/g, '\\"')}" | |
| } | |
| `; | |
| const response = await model.invoke(prompt); | |
| const jsonText = response.content.replace(/```json|```/g, "").trim(); | |
| const structured = JSON.parse(jsonText); | |
| structured.sources = allSources; | |
| structured.verification_rate = `${( | |
| (state.groundedFacts.length / | |
| (state.groundedFacts.length + state.groundingIssues.length)) * | |
| 100 | |
| ).toFixed(0)}%`; | |
| if (state.corrections && state.corrections.length > 0) { | |
| structured.corrections_applied = state.corrections.map((c) => c.problem); | |
| } | |
| console.log(`β Structured output created`); | |
| console.log(` Category: ${structured.category}`); | |
| console.log(` Confidence: ${structured.confidence_score}`); | |
| console.log(` Sources: ${allSources.length}`); | |
| return { | |
| finalJson: structured, | |
| sources: allSources, | |
| }; | |
| } | |
| // ======================================== | |
| // BUILD LANGGRAPH | |
| // ======================================== | |
| const workflow = new StateGraph({ channels: graphState }) | |
| .addNode("planner", planActions) | |
| .addNode("grounder", groundFacts) | |
| .addNode("validator", validatePlan) | |
| .addNode("executor", executeAnswer) | |
| .addNode("structurer", structureOutput) | |
| .addEdge(START, "planner") | |
| .addEdge("planner", "grounder") | |
| .addEdge("grounder", "validator") | |
| .addEdge("validator", "executor") | |
| .addEdge("executor", "structurer") | |
| .addEdge("structurer", END); | |
| const appFlow = workflow.compile(); | |
| console.log("β LangGraph compiled successfully\n"); | |
| // ======================================== | |
| // API ENDPOINT | |
| // ======================================== | |
| app.post("/query", async (req, res) => { | |
| const { question } = req.body; | |
| console.log("\n" + "=".repeat(70)); | |
| console.log(`π NEW REQUEST: "${question}"`); | |
| console.log("=".repeat(70)); | |
| try { | |
| const result = await appFlow.invoke({ userQuery: question }); | |
| console.log("\n" + "=".repeat(70)); | |
| console.log("β FINAL OUTPUT:"); | |
| console.log(JSON.stringify(result.finalJson, null, 2)); | |
| console.log("=".repeat(70) + "\n"); | |
| if (result.finalJson.validation_status === "REJECTED") { | |
| return res.status(422).json(result.finalJson); | |
| } | |
| res.json(result.finalJson); | |
| } catch (error) { | |
| console.error("\nβ ERROR:", error.message); | |
| console.error(error.stack); | |
| res.status(500).json({ | |
| error: "System failure", | |
| details: error.message, | |
| }); | |
| } | |
| }); | |
| app.listen(PORT, () => { | |
| console.log("=".repeat(70)); | |
| console.log(`π Hybrid System running on port ${PORT}`); | |
| console.log("=".repeat(70)); | |
| console.log("\nArchitecture:"); | |
| console.log(" Plan β Ground β Validate β Execute β Structure"); | |
| console.log("\nDeterministic Validation:"); | |
| console.log(" β Fact verification rate"); | |
| console.log(" β Source quality scoring"); | |
| console.log(" β No LLM self-validation"); | |
| console.log("\nEndpoint: POST http://localhost:3000/query"); | |
| console.log(' Body: {"question": "your query"}'); | |
| console.log("=".repeat(70) + "\n"); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment