From 5855ed89fdd5be55b403441e42e15ee47163d301 Mon Sep 17 00:00:00 2001 From: Delacrobix Date: Mon, 13 Oct 2025 09:32:31 -0500 Subject: [PATCH 1/2] supporting blog content langgraph-js-elasticsearch --- .../langgraph-js-elasticsearch/dataset.json | 222 ++++++ .../elasticsearchSetup.ts | 220 ++++++ .../langgraph-js-elasticsearch/main.ts | 327 +++++++++ .../langgraph-js-elasticsearch/package.json | 20 + .../responses/aggregationsResponse.json | 263 ++++++++ .../workflow_graph.png | Bin 0 -> 25397 bytes .../langgraph-js-elasticsearch/yarn.lock | 638 ++++++++++++++++++ 7 files changed, 1690 insertions(+) create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/dataset.json create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/main.ts create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/package.json create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/workflow_graph.png create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/yarn.lock diff --git a/supporting-blog-content/langgraph-js-elasticsearch/dataset.json b/supporting-blog-content/langgraph-js-elasticsearch/dataset.json new file mode 100644 index 00000000..7728ff93 --- /dev/null +++ b/supporting-blog-content/langgraph-js-elasticsearch/dataset.json @@ -0,0 +1,222 @@ +[ + { + "description": "TechFlow optimizes supply chain operations using AI-powered route optimization and real-time tracking. Founded in 2023, shows remarkable growth with $500K monthly revenue.", + "company_name": "TechFlow", + "industry": "logistics", + "location": "San Francisco, CA", + "funding_stage": "Series A", + "funding_amount": 8000000, + "lead_investor": "Sequoia Capital", + "monthly_revenue": 500000, + "business_model": "B2B" + }, + { + "description": "UrbanMobility revolutionizes urban transportation through autonomous delivery drones and smart logistics hubs. Partners with major retailers for same-day delivery across Manhattan and Brooklyn.", + "company_name": "UrbanMobility", + "industry": "logistics", + "location": "New York, NY", + "funding_stage": "Series B", + "funding_amount": 15000000, + "lead_investor": "Kleiner Perkins", + "monthly_revenue": 750000, + "business_model": "B2B2C" + }, + { + "description": "FinanceAI provides AI-powered investment advisory services to retail investors. Uses machine learning to analyze market trends with over 100,000 active users.", + "company_name": "FinanceAI", + "industry": "fintech", + "location": "San Francisco, CA", + "funding_stage": "Series C", + "funding_amount": 25000000, + "lead_investor": "Tiger Global Management", + "monthly_revenue": 1200000, + "business_model": "B2C" + }, + { + "description": "HealthTech Solutions develops medical devices and software for remote patient monitoring. Comprehensive telehealth platform reducing hospital readmissions by 30%.", + "company_name": "HealthTech Solutions", + "industry": "healthcare", + "location": "Boston, MA", + "funding_stage": "Series B", + "funding_amount": 18000000, + "lead_investor": "General Catalyst", + "monthly_revenue": 900000, + "business_model": "B2B" + }, + { + "description": "SmartWarehouse transforms traditional warehouses into automated facilities using robotics and AI. Robotic systems pick, pack, and sort 5x faster than human workers.", + "company_name": "SmartWarehouse", + "industry": "logistics", + "location": "New York, NY", + "funding_stage": "Seed", + "funding_amount": 5000000, + "lead_investor": "Founders Fund", + "monthly_revenue": 300000, + "business_model": "B2B" + }, + { + "description": "EcoTransport provides carbon-neutral delivery services using electric vehicles and optimized routing algorithms. Popular with environmentally conscious brands.", + "company_name": "EcoTransport", + "industry": "logistics", + "location": "San Francisco, CA", + "funding_stage": "Series A", + "funding_amount": 12000000, + "lead_investor": "NEA (New Enterprise Associates)", + "monthly_revenue": 650000, + "business_model": "B2B" + }, + { + "description": "CyberSecure develops AI-powered threat detection and response systems for enterprise clients. Platform identifies and neutralizes cyber threats 10x faster than traditional systems.", + "company_name": "CyberSecure", + "industry": "cybersecurity", + "location": "Austin, TX", + "funding_stage": "Series B", + "funding_amount": 20000000, + "lead_investor": "Insight Partners", + "monthly_revenue": 1100000, + "business_model": "B2B" + }, + { + "description": "DataViz creates intuitive data visualization tools for enterprise customers. No-code platform allows business users to create dashboards without technical expertise.", + "company_name": "DataViz", + "industry": "enterprise software", + "location": "New York, NY", + "funding_stage": "Series A", + "funding_amount": 10000000, + "lead_investor": "Battery Ventures", + "monthly_revenue": 450000, + "business_model": "B2B" + }, + { + "description": "CloudNative builds kubernetes-native development tools for cloud deployment and management. Platform reduces deployment time by 80% for developers.", + "company_name": "CloudNative", + "industry": "developer tools", + "location": "Seattle, WA", + "funding_stage": "Seed", + "funding_amount": 6000000, + "lead_investor": "Madrona Venture Group", + "monthly_revenue": 250000, + "business_model": "B2B" + }, + { + "description": "LastMile Logistics revolutionizes final-mile delivery through micro-fulfillment centers and crowdsourced drivers. Uses predictive analytics for under 30-minute delivery times.", + "company_name": "LastMile Logistics", + "industry": "logistics", + "location": "San Francisco, CA", + "funding_stage": "Series A", + "funding_amount": 14000000, + "lead_investor": "Benchmark Capital", + "monthly_revenue": 580000, + "business_model": "B2B2C" + }, + { + "description": "TechMed develops AI-powered diagnostic tools for hospitals and clinics. Platform improves diagnostic accuracy by 45% using computer vision and machine learning.", + "company_name": "TechMed", + "industry": "healthcare", + "location": "Los Angeles, CA", + "funding_stage": "Series A", + "funding_amount": 9000000, + "lead_investor": "GV (Google Ventures)", + "monthly_revenue": 380000, + "business_model": "B2B" + }, + { + "description": "CryptoWallet provides secure digital wallet solutions for cryptocurrency trading and storage. Multi-chain support with enterprise-grade security features.", + "company_name": "CryptoWallet", + "industry": "fintech", + "location": "Miami, FL", + "funding_stage": "Series B", + "funding_amount": 16000000, + "lead_investor": "Coinbase Ventures", + "monthly_revenue": 820000, + "business_model": "B2C" + }, + { + "description": "GreenEnergy develops solar panel optimization software for residential and commercial installations. AI algorithms increase energy efficiency by 35%.", + "company_name": "GreenEnergy", + "industry": "cleantech", + "location": "Denver, CO", + "funding_stage": "Series B", + "funding_amount": 22000000, + "lead_investor": "Energy Impact Partners", + "monthly_revenue": 95000, + "business_model": "B2C" + }, + { + "description": "EduTech creates interactive learning platforms for K-12 education. Gamified learning experiences improve student engagement by 60% across 500+ schools.", + "company_name": "EduTech", + "industry": "edtech", + "location": "Chicago, IL", + "funding_stage": "Series A", + "funding_amount": 11000000, + "lead_investor": "Reach Capital", + "monthly_revenue": 420000, + "business_model": "B2B" + }, + { + "description": "RoboFarm develops autonomous farming robots for precision agriculture. Robots plant, monitor, and harvest crops with 90% less water usage than traditional farming.", + "company_name": "RoboFarm", + "industry": "agtech", + "location": "Portland, OR", + "funding_stage": "Seed", + "funding_amount": 7000000, + "lead_investor": "S2G Ventures", + "monthly_revenue": 290000, + "business_model": "B2B" + }, + { + "description": "VRFitness creates immersive virtual reality fitness experiences for home users. Platform offers 200+ workout programs with real-time coaching and social features.", + "company_name": "VRFitness", + "industry": "fitness tech", + "location": "San Diego, CA", + "funding_stage": "Seed", + "funding_amount": 4500000, + "lead_investor": "Bullpen Capital", + "monthly_revenue": 180000, + "business_model": "B2C" + }, + { + "description": "SmartRetail provides AI-powered inventory management and customer analytics for retail chains. Platform reduces inventory costs by 25% while improving customer satisfaction.", + "company_name": "SmartRetail", + "industry": "retail tech", + "location": "Atlanta, GA", + "funding_stage": "Series B", + "funding_amount": 19000000, + "lead_investor": "Norwest Venture Partners", + "monthly_revenue": 780000, + "business_model": "B2B" + }, + { + "description": "PropTech Solutions develops smart building management systems for commercial real estate. IoT sensors and AI optimize energy usage and maintenance schedules.", + "company_name": "PropTech Solutions", + "industry": "proptech", + "location": "Phoenix, AZ", + "funding_stage": "Series A", + "funding_amount": 13000000, + "lead_investor": "Fifth Wall", + "monthly_revenue": 540000, + "business_model": "B2B" + }, + { + "description": "GameStudio creates mobile gaming experiences with blockchain integration. Free-to-play games with NFT rewards have reached 2 million active users.", + "company_name": "GameStudio", + "industry": "gaming", + "location": "Las Vegas, NV", + "funding_stage": "Series A", + "funding_amount": 12000000, + "lead_investor": "Bitkraft Ventures", + "monthly_revenue": 680000, + "business_model": "B2C" + }, + { + "description": "SpaceTech develops satellite communication systems for rural internet connectivity. Low-orbit satellites provide high-speed internet to underserved areas globally.", + "company_name": "SpaceTech", + "industry": "aerospace", + "location": "Houston, TX", + "funding_stage": "Series C", + "funding_amount": 35000000, + "lead_investor": "Space Capital", + "monthly_revenue": 1400000, + "business_model": "B2B2C" + } +] diff --git a/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts b/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts new file mode 100644 index 00000000..7d1c09b0 --- /dev/null +++ b/supporting-blog-content/langgraph-js-elasticsearch/elasticsearchSetup.ts @@ -0,0 +1,220 @@ +import fs from "node:fs/promises"; +import { Client } from "@elastic/elasticsearch"; +import { z } from "zod"; +import dotenv from "dotenv"; + +dotenv.config(); + +const INDEX_NAME: string = "startups-index"; +const INVESTMENT_FOCUSED_TEMPLATE = "investment-focused-template"; +const MARKET_FOCUSED_TEMPLATE = "market-focused-template"; +const ELASTICSEARCH_ENDPOINT: string = process.env.ELASTICSEARCH_ENDPOINT ?? ""; +const ELASTICSEARCH_API_KEY: string = process.env.ELASTICSEARCH_API_KEY ?? ""; + +const esClient = new Client({ + node: ELASTICSEARCH_ENDPOINT, + auth: { + apiKey: ELASTICSEARCH_API_KEY, + }, +}); + +const StartupDocumentSchema = z.object({ + description: z.string(), + semantic_field: z.string(), + company_name: z.string(), + industry: z.string(), + location: z.string(), + funding_stage: z.string(), + funding_amount: z.number(), + lead_investor: z.string(), + monthly_revenue: z.number(), + business_model: z.string(), +}); + +type StartupDocumentType = z.infer; + +async function loadDataset(path: string): Promise { + const raw = await fs.readFile(path, "utf-8"); + const data = JSON.parse(raw); + + return data; +} + +async function createIndex() { + const indexExists = await esClient.indices.exists({ index: INDEX_NAME }); + + if (indexExists) { + console.log("✅ Index already exists."); + return; + } + + await esClient.indices.create({ + index: INDEX_NAME, + mappings: { + properties: { + description: { + type: "text", + copy_to: "semantic_field", + }, + company_name: { + type: "keyword", + copy_to: "semantic_field", + }, + industry: { + type: "keyword", + copy_to: "semantic_field", + }, + location: { + type: "keyword", + copy_to: "semantic_field", + }, + funding_stage: { + type: "keyword", + copy_to: "semantic_field", + }, + funding_amount: { type: "long" }, + lead_investor: { + type: "keyword", + copy_to: "semantic_field", + }, + monthly_revenue: { type: "long" }, + business_model: { + type: "keyword", + copy_to: "semantic_field", + }, + semantic_field: { type: "semantic_text" }, + }, + }, + }); + + console.log("✅ Index created successfully!"); +} + +async function ingestDocuments() { + const indexCount = await esClient.count({ index: INDEX_NAME }); + const documentCount = indexCount.count; + + if (documentCount == 0) { + const datasetPath = "./dataset.json"; + const documents = await loadDataset(datasetPath); + + console.log("Ingesting documents..."); + + try { + const body = documents.flatMap((doc) => [ + { index: { _index: INDEX_NAME } }, + doc, + ]); + + await esClient.bulk({ body }); + + console.log("✅ Documents ingested successfully!"); + } catch (error: any) { + console.error("❌ Error during ingestion:", error.message); + } + } +} + +// Register RRF hybrid search templates in Elasticsearch +async function createSearchTemplates() { + try { + await esClient.putScript({ + id: INVESTMENT_FOCUSED_TEMPLATE, + script: { + lang: "mustache", + source: `{ + "size": 5, + "retriever": { + "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "semantic": { + "field": "semantic_field", + "query": "{{query_text}}" + } + } + } + }, + { + "standard": { + "query": { + "bool": { + "filter": [ + {"terms": {"funding_stage": {{#join}}{{#toJson}}funding_stage{{/toJson}}{{/join}}}}, + {"range": {"funding_amount": {"gte": {{funding_amount_gte}}{{#funding_amount_lte}},"lte": {{funding_amount_lte}}{{/funding_amount_lte}}}}}, + {"terms": {"lead_investor": {{#join}}{{#toJson}}lead_investor{{/toJson}}{{/join}}}}, + {"range": {"monthly_revenue": {"gte": {{monthly_revenue_gte}}{{#monthly_revenue_lte}},"lte": {{monthly_revenue_lte}}{{/monthly_revenue_lte}}}}} + ] + } + } + } + } + ], + "rank_window_size": 100, + "rank_constant": 20 + } + } + }`, + }, + }); + + console.log("✅ Investment-focused template created successfully!"); + + await esClient.putScript({ + id: MARKET_FOCUSED_TEMPLATE, + script: { + lang: "mustache", + source: `{ + "size": 5, + "retriever": { + "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "semantic": { + "field": "semantic_field", + "query": "{{query_text}}" + } + } + } + }, + { + "standard": { + "query": { + "bool": { + "filter": [ + {"terms": {"industry": {{#join}}{{#toJson}}industry{{/toJson}}{{/join}}}}, + {"terms": {"location": {{#join}}{{#toJson}}location{{/toJson}}{{/join}}}}, + {"terms": {"business_model": {{#join}}{{#toJson}}business_model{{/toJson}}{{/join}}}} + ] + } + } + } + } + ], + "rank_window_size": 50, + "rank_constant": 10 + } + } + }`, + }, + }); + + console.log("✅ Market-focused template created successfully!"); + } catch (error) { + console.error("❌ Error creating RRF templates:", error); + } +} + +export { + createIndex, + ingestDocuments, + createSearchTemplates, + esClient, + INDEX_NAME, + INVESTMENT_FOCUSED_TEMPLATE, + MARKET_FOCUSED_TEMPLATE, +}; diff --git a/supporting-blog-content/langgraph-js-elasticsearch/main.ts b/supporting-blog-content/langgraph-js-elasticsearch/main.ts new file mode 100644 index 00000000..becb1c36 --- /dev/null +++ b/supporting-blog-content/langgraph-js-elasticsearch/main.ts @@ -0,0 +1,327 @@ +import { writeFileSync } from "node:fs"; +import { StateGraph, Annotation, START, END } from "@langchain/langgraph"; +import { ChatOpenAI } from "@langchain/openai"; +import { z } from "zod"; +import { + esClient, + ingestDocuments, + createSearchTemplates, + INDEX_NAME, + INVESTMENT_FOCUSED_TEMPLATE, + MARKET_FOCUSED_TEMPLATE, + createIndex, +} from "./elasticsearchSetup.js"; + +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); + +// Define state for workflow +const VCState = Annotation.Root({ + input: Annotation(), // User's natural language query + searchStrategy: Annotation(), // Search strategy chosen by LLM + searchParams: Annotation(), // Prepared search parameters + results: Annotation(), // Search results + final: Annotation(), // Final formatted response +}); + +// Extract available filter values from Elasticsearch using aggregations +async function getAvailableFilters() { + try { + const response = await esClient.search({ + index: INDEX_NAME, + size: 0, + aggs: { + industries: { + terms: { field: "industry", size: 100 }, + }, + locations: { + terms: { field: "location", size: 100 }, + }, + funding_stages: { + terms: { field: "funding_stage", size: 20 }, + }, + business_models: { + terms: { field: "business_model", size: 10 }, + }, + lead_investors: { + terms: { field: "lead_investor", size: 100 }, + }, + funding_amount_stats: { + stats: { field: "funding_amount" }, + }, + }, + }); + + console.log( + "✅ Available filters:", + JSON.stringify(response.aggregations, null, 2) + ); + + return response.aggregations; + } catch (error) { + console.error("❌ Error getting available filters:", error); + + return {}; + } +} + +// Node 1: Decide search strategy using LLM +async function decideSearchStrategy(state: typeof VCState.State) { + // Zod schema for specialized search strategy decision + const SearchDecisionSchema = z.object({ + search_type: z + .enum(["investment_focused", "market_focused"]) + .describe("Type of specialized search strategy to use"), + reasoning: z + .string() + .describe("Brief explanation of why this search strategy was chosen"), + }); + + const decisionLLM = llm.withStructuredOutput(SearchDecisionSchema); + + // Get dynamic filters from Elasticsearch + const availableFilters = await getAvailableFilters(); + + const prompt = `Query: "${state.input}" + Available filters: ${JSON.stringify(availableFilters, null, 2)} + + Choose between two specialized search strategies: + + - investment_focused: For queries about funding stages, funding amounts, monthly revenue, lead investors, financial performance + + - market_focused: For queries about industries, locations, business models, market segments, geographic markets + + Analyze the query intent and choose the most appropriate strategy. + `; + + try { + const result = await decisionLLM.invoke(prompt); + console.log( + `🤔 Search strategy: ${result.search_type} - ${result.reasoning}` + ); + + return { + searchStrategy: result.search_type, + }; + } catch (error: any) { + console.error("❌ Error in decideSearchStrategy:", error.message); + return { + searchStrategy: "investment_focused", + }; + } +} + +// Extract all possible filter values from user input +async function extractFilterValues(input: string) { + const FilterValuesSchema = z.object({ + // Investment-focused filters + funding_stage: z + .array(z.string()) + .default([]) + .describe("Funding stage values mentioned in query"), + funding_amount_gte: z + .number() + .default(0) + .describe("Minimum funding amount in USD"), + funding_amount_lte: z + .number() + .default(100000000) + .describe("Maximum funding amount in USD"), + lead_investor: z + .array(z.string()) + .default([]) + .describe("Lead investor values mentioned in query"), + monthly_revenue_gte: z + .number() + .default(0) + .describe("Minimum monthly revenue in USD"), + monthly_revenue_lte: z + .number() + .default(10000000) + .describe("Maximum monthly revenue in USD"), + industry: z + .array(z.string()) + .default([]) + .describe("Industry values mentioned in query"), + location: z + .array(z.string()) + .default([]) + .describe("Location values mentioned in query"), + business_model: z + .array(z.string()) + .default([]) + .describe("Business model values mentioned in query"), + }); + + const extractorLLM = llm.withStructuredOutput(FilterValuesSchema); + const availableFilters = await getAvailableFilters(); + + const extractPrompt = `Extract ALL relevant filter values from: "${input}" + Available options: ${JSON.stringify(availableFilters, null, 2)} + Extract only values explicitly mentioned in the query. Leave fields empty if not mentioned.`; + + return await extractorLLM.invoke(extractPrompt); +} + +// Node 2A: Prepare Investment-Focused Search Parameters +async function prepareInvestmentSearch(state: typeof VCState.State) { + console.log( + "💰 Preparing INVESTMENT-FOCUSED search parameters with financial emphasis..." + ); + + try { + // Extract all filter values from input + const values = await extractFilterValues(state.input); + + let searchParams: any = { + template_id: INVESTMENT_FOCUSED_TEMPLATE, + query_text: state.input, + ...values, + }; + + return { searchParams }; + } catch (error) { + console.error("❌ Error preparing investment-focused params:", error); + return { + searchParams: {}, + }; + } +} + +// Node 2B: Prepare Market-Focused Search Parameters +async function prepareMarketSearch(state: typeof VCState.State) { + console.log( + "🔍 Preparing MARKET-FOCUSED search parameters with market emphasis..." + ); + + try { + // Extract all filter values from input + const values = await extractFilterValues(state.input); + + let searchParams: any = { + template_id: MARKET_FOCUSED_TEMPLATE, + query_text: state.input, + ...values, + }; + + return { searchParams }; + } catch (error) { + console.error("❌ Error preparing market-focused params:", error); + return {}; + } +} + +// Node 3: Execute Search +async function executeSearch(state: typeof VCState.State) { + const { searchParams } = state; + + try { + // getting formed query from template for debugging + const renderedTemplate = await esClient.renderSearchTemplate({ + id: searchParams.template_id, + params: searchParams, + }); + + console.log( + "📋 Complete query:", + JSON.stringify(renderedTemplate.template_output, null, 2) + ); + + const results = await esClient.searchTemplate({ + index: INDEX_NAME, + id: searchParams.template_id, + params: searchParams, + }); + + return { + results: results.hits.hits.map((hit: any) => hit._source), + }; + } catch (error: any) { + console.error(`❌ ${state.searchParams.search_type} search error:`, error); + return { results: [] }; + } +} + +// Node 4: Visualize results +async function visualizeResults(state: typeof VCState.State) { + const results = state.results || []; + + let formattedResults = `🎯 Found ${results.length} startups matching your criteria:\n\n`; + + results.forEach((startup: any, index: number) => { + formattedResults += `${index + 1}. **${startup.company_name}**\n`; + formattedResults += ` 📍 ${startup.location} | 🏢 ${startup.industry} | 💼 ${startup.business_model}\n`; + formattedResults += ` 💰 ${startup.funding_stage} - $${( + startup.funding_amount / 1000000 + ).toFixed(1)}M\n`; + formattedResults += ` $${(startup.monthly_revenue / 1000).toFixed( + 0 + )}K MRR\n`; + formattedResults += ` 🏦 Lead: ${startup.lead_investor}\n`; + formattedResults += ` 📝 ${startup.description}\n\n`; + }); + + return { + final: formattedResults, + }; +} + +async function saveGraphImage(app: any): Promise { + try { + const drawableGraph = app.getGraph(); + const image = await drawableGraph.drawMermaidPng(); + const arrayBuffer = await image.arrayBuffer(); + + const filePath = "./workflow_graph.png"; + writeFileSync(filePath, new Uint8Array(arrayBuffer)); + console.log(`📊 Workflow graph saved as: ${filePath}`); + } catch (error: any) { + console.log("⚠️ Could not save graph image:", error.message); + } +} + +async function main() { + await createIndex(); + await createSearchTemplates(); + await ingestDocuments(); + + // Create the workflow graph with shared state + const workflow = new StateGraph(VCState) + // Register nodes - these are the processing functions + .addNode("decideStrategy", decideSearchStrategy) + .addNode("prepareInvestment", prepareInvestmentSearch) + .addNode("prepareMarket", prepareMarketSearch) + .addNode("executeSearch", executeSearch) + .addNode("visualizeResults", visualizeResults) + // Define execution flow with conditional branching + .addEdge(START, "decideStrategy") // Start with strategy decision + .addConditionalEdges( + "decideStrategy", + (state: typeof VCState.State) => state.searchStrategy, // Conditional function + { + investment_focused: "prepareInvestment", // If investment focused -> RRF template preparation + market_focused: "prepareMarket", // If market focused -> dynamic query preparation + } + ) + .addEdge("prepareInvestment", "executeSearch") // Investment prep -> execute + .addEdge("prepareMarket", "executeSearch") // Market prep -> execute + .addEdge("executeSearch", "visualizeResults") // Execute -> visualize + .addEdge("visualizeResults", END); // End workflow + + const app = workflow.compile(); + + await saveGraphImage(app); + + // Investment-focused query (emphasizes funding, revenue, financial metrics) + const query = + "Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K"; + + // Market-focused query (emphasizes industry, geography, market positioning) + // const query = + // "Find fintech and healthcare startups in San Francisco, New York, or Boston"; + // console.log("🔍 Query:", query); + + const marketResult = await app.invoke({ input: query }); + console.log(marketResult.final); +} + +main().catch(console.error); diff --git a/supporting-blog-content/langgraph-js-elasticsearch/package.json b/supporting-blog-content/langgraph-js-elasticsearch/package.json new file mode 100644 index 00000000..ede545e9 --- /dev/null +++ b/supporting-blog-content/langgraph-js-elasticsearch/package.json @@ -0,0 +1,20 @@ +{ + "name": "js-app", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "tsx main.ts" + }, + "dependencies": { + "@elastic/elasticsearch": "^9.1.1", + "@langchain/core": "^0.3.78", + "@langchain/langgraph": "^0.4.9", + "@langchain/openai": "^0.6.14", + "@types/node": "^24.6.0", + "dotenv": "^17.2.2", + "tsx": "^4.20.6", + "typescript": "^5.9.2", + "zod": "^3.23.8" + } +} diff --git a/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json b/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json new file mode 100644 index 00000000..e33c7b52 --- /dev/null +++ b/supporting-blog-content/langgraph-js-elasticsearch/responses/aggregationsResponse.json @@ -0,0 +1,263 @@ +{ + "industries": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "logistics", + "doc_count": 5 + }, + { + "key": "fintech", + "doc_count": 2 + }, + { + "key": "healthcare", + "doc_count": 2 + }, + { + "key": "aerospace", + "doc_count": 1 + }, + { + "key": "agtech", + "doc_count": 1 + }, + { + "key": "cleantech", + "doc_count": 1 + }, + { + "key": "cybersecurity", + "doc_count": 1 + }, + { + "key": "developer tools", + "doc_count": 1 + }, + { + "key": "edtech", + "doc_count": 1 + }, + { + "key": "enterprise software", + "doc_count": 1 + }, + { + "key": "fitness tech", + "doc_count": 1 + }, + { + "key": "gaming", + "doc_count": 1 + }, + { + "key": "proptech", + "doc_count": 1 + }, + { + "key": "retail tech", + "doc_count": 1 + } + ] + }, + "locations": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "San Francisco, CA", + "doc_count": 4 + }, + { + "key": "New York, NY", + "doc_count": 3 + }, + { + "key": "Atlanta, GA", + "doc_count": 1 + }, + { + "key": "Austin, TX", + "doc_count": 1 + }, + { + "key": "Boston, MA", + "doc_count": 1 + }, + { + "key": "Chicago, IL", + "doc_count": 1 + }, + { + "key": "Denver, CO", + "doc_count": 1 + }, + { + "key": "Houston, TX", + "doc_count": 1 + }, + { + "key": "Las Vegas, NV", + "doc_count": 1 + }, + { + "key": "Los Angeles, CA", + "doc_count": 1 + }, + { + "key": "Miami, FL", + "doc_count": 1 + }, + { + "key": "Phoenix, AZ", + "doc_count": 1 + }, + { + "key": "Portland, OR", + "doc_count": 1 + }, + { + "key": "San Diego, CA", + "doc_count": 1 + }, + { + "key": "Seattle, WA", + "doc_count": 1 + } + ] + }, + "funding_stages": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "Series A", + "doc_count": 8 + }, + { + "key": "Series B", + "doc_count": 6 + }, + { + "key": "Seed", + "doc_count": 4 + }, + { + "key": "Series C", + "doc_count": 2 + } + ] + }, + "business_models": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "B2B", + "doc_count": 13 + }, + { + "key": "B2C", + "doc_count": 4 + }, + { + "key": "B2B2C", + "doc_count": 3 + } + ] + }, + "lead_investors": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "Battery Ventures", + "doc_count": 1 + }, + { + "key": "Benchmark Capital", + "doc_count": 1 + }, + { + "key": "Bitkraft Ventures", + "doc_count": 1 + }, + { + "key": "Bullpen Capital", + "doc_count": 1 + }, + { + "key": "Coinbase Ventures", + "doc_count": 1 + }, + { + "key": "Energy Impact Partners", + "doc_count": 1 + }, + { + "key": "Fifth Wall", + "doc_count": 1 + }, + { + "key": "Founders Fund", + "doc_count": 1 + }, + { + "key": "GV (Google Ventures)", + "doc_count": 1 + }, + { + "key": "General Catalyst", + "doc_count": 1 + }, + { + "key": "Insight Partners", + "doc_count": 1 + }, + { + "key": "Kleiner Perkins", + "doc_count": 1 + }, + { + "key": "Madrona Venture Group", + "doc_count": 1 + }, + { + "key": "NEA (New Enterprise Associates)", + "doc_count": 1 + }, + { + "key": "Norwest Venture Partners", + "doc_count": 1 + }, + { + "key": "Reach Capital", + "doc_count": 1 + }, + { + "key": "S2G Ventures", + "doc_count": 1 + }, + { + "key": "Sequoia Capital", + "doc_count": 1 + }, + { + "key": "Space Capital", + "doc_count": 1 + }, + { + "key": "Tiger Global Management", + "doc_count": 1 + } + ] + }, + "funding_amount_stats": { + "count": 20, + "min": 4500000, + "max": 35000000, + "avg": 14075000, + "sum": 281500000 + } +} diff --git a/supporting-blog-content/langgraph-js-elasticsearch/workflow_graph.png b/supporting-blog-content/langgraph-js-elasticsearch/workflow_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..0d8af8610d6edc667d1fa3f6aa4cfe3bed3625c5 GIT binary patch literal 25397 zcmb5WWmFwO*EJX*xD({!?rwqL?!gJ}?iSqL-GjRZcXxLQ65QQgr}Mn?%r`S@X4V=O zKkmA8byam$pW0{deF&466-R`_f&2976QZPqh~lSDV3wagfz!f(1K$)SGOmC6gz`yJ zL{Ql^<1`am09_nw$n@e+q9(7TMDIfEl>HvZ@)Sq&6vr^C*DO}{4n9O;ZfIQnJZ5P5=04Qj6YH3;9NxCjph9!6c0i{2? zc<~oODu-)p^!zu^omQ_Zb#W~93y;LpI6{_x&)Gw79i*TKY+(Z0%Tqq+{B6al1&@@4Bf4jGv-NT0-3|KxbW z0h1vu0GpNYEx+;|AMX&0G=-XnW|`wm!50+@mO7NW+AY{tE3(4MI>db=Z3W}yePD`1 z*4tnDc1O!a3`9^sH27UaZTwG~EgWXkIa37_tG28y^5cb#Q9gm&m@5}^Z!8fcv%$8Z z@d?FqSB*mLU4qxkBwc*pNIWVO1a)W)8am3`!E94sGP>!0|H{?+9rr$s zE-e-*0t75+WI~7ZdLy=x^V5KSM1=0r12qdvFlsOub*MtE`7hht_^&qPXK7CgHJv+? zEwRb)Fc5 zKrjI~10yY-M?d=tjk?wGNc+2C{d8dmBi1M45gY^1fg ziYTO{Tv`fU`JMQ&Mc>n(Um`j;yF8z2zo&ntvn*Nc5ydQqxh__tNGFD5rm)F|Pu8f; zc(O_(fQ5m1e0^g5GL=9nr}o3Zn6|}HHYFG}4Td_D`Rd*+u|fMsCQFb|jpbq`0k8Xf zsZz0Ol}@>8Rq}{Ylg*mN-$wf6M$1K;)1?}jJ#{**=4!`-$p*_su^O+J`>S#f;K$uv zr}by4Xawe3n~N^crb4|A90GzFxGs|pzS=Pssw=`!q)})Jnwf&6ciZ~3kj`?c&U9*j zB3(R&01@(=&-?2ycFWVVvzNP*CW8UEah{)t@;&~L3RXw6g%|4`=`6-@L-yX@-tr0H zkZ^h!($aeq`?wr7r$B#uqe){)v{<;f9>?>g`QkCP6~yAQN5!8MW37VG`%_~FwtLf6 z_=YpNT}G4X$4nWxfLs68XkgUsEMREV>+v5RmQYnymFer^^nAQbqE*cy8a4gG{G-lf zB8%69r9;oqlsJMCa>PVJFfxqp4OXd8D(S~>4R(+FQyP`B`6}HmdEyC|3gRWdCWoi% zJ>bb*Il68H7R#iu$mmMM^WR1eV**WK;iVZEbS7(|j08{`wVLg~p5TF>$gW5nb{gCD z_G9B+hbKAyx4=uI{!mmq0X#fBC_@TW4X@?$m0h*g(%vQ}qB5 zlB1m7p`nApDBP39s+iDFwNB3#_cYU~EMD(d*Rz#|g>uz{gTRoGgRzttW9T^It3Qow z=&=*iuuK$i=nYZPu{9N7)N+w^dcA9F9hyxxAUU`9SGRB$Vq)S)bh18i$T5mu;hoHz z8FHroY$%I{NzxQnb*<#k;jk~1gE53YSK9;A)A9(TS==s2X{h6j8ue6sa@l-uhcl$1 z@ZcF|f15&rg7k?@fa@eOp+0{$tC*gcpr@jO>EjX_By$XlGT5`2BOR;71~xWC+{eh- zg*qma)3Gv4e=4&f4u=ho>0|~JB97iL=1rWU_20&`R;QC#(}`^UkJT1OLO#!>5{3M1 zpLe&%%PnAagKIZgor0WB{tQ%~H{0*XX7P-$r2jPyz#a-uVtxy$!{)q=WFg&oO#akG z87GXDzL-CvSZCf^)sbKSmy<)xh;}ysgiZfKoh^O8vBqRFIBZl~Vuw8vLM#wQwbR$o ze8%tNO{YGfuru3ayK#TKAiYDf735TEzR?LxIA8sb@2`uswqpCZgoH1%3^-WCQWlGq znS-L-nC+t~zA}qMIaGsGPuA@&X*ftY8wV6AW2B^{?3EZ87{-?6PpWRo$;n^8s^3lf z0vog2a=Mv{fg!pJpO5{|pFcl-{3t#6aG~9upUgoWo8XORMFDnH{AFyab=?Mnu1R!< z*%{}Bd*=0z6487nD6l=jNHl0u%c=dz6AE`$R>Ph7lyi(z2|fhqYr)7=mre!fA5ZBq zl(i-mE;s881GIB6m0*H!2CJ*QA4j8Gaw_Rui&kkNX$(^~K^cF)?CSJ%4j|vc+4T9N z_a8ppG9OP3yf-4vVtU=Qq5nDA8N2O^hV<6&x!Q0ZQ7UR>B}G60LcadTF^8WwbK|#y z!m@AkHr89%2 zJMmdo6mfRPF$?l*q`jpSASiN4m%yOod^Ygy`r6DqE5NZ5C%VNmMpG*cHB1Mw6P z1v1S4)_k}EX&H>`3fBo>m{D1Z%Pm%P0xen@j zYjd+3=HDE^f`m}f(0c#p^CJ^z{%4~92K4{Gc{epWoEXZH-u3RtP&7UOqh#)4iDiS5k`{oFh^=2K0TL{T z$rTR4q*fHk6QW7l{sOmsd^`%n3Orm1#vpjCO4X{Y*12%IBIy){NG3yI)Tp=TYq(q_ zo-YB{ zz<*aoA@-EnI6-F-dtC zLo3|bAkJYr$etSR<+=0r>dMj4kyaru@Y(a7l{~}ZZTIf-M!gKP%ob*BZ^i1%%NS=1 z=G?-MV&8e8IMyoTyL~rMoW6wM?daH0u4$uBKXFs_u0ehRV}QRzs!XzHRw8x$ltOf& z6sMbKZC%l+trncNe3rUwFZwa7TV|{S9J~7VSV1gCuEt<|n7{mQts=aM?+UQGK4lt- z8aaYU%5c4^!>NPcE!ijMU(&5gMbsT}oUP3Zl{HU$Iim0^>&J&e5d>Tkk75=-MDaGR zYcvwHj4k&JM4WdXZs+#ivQIPUXy+$K0Ax%kBIo&xQsbeomwhyVtuJw!aiCqSJqhRJ zz$!#Zl~6_*ZHwqIDNc~xv!yuxyie^OP<$Tie;77ARqIb~wmG*+rM|%Y4BxWVDy%BX zi;9(BUEgRB=%;_8mqeoaJ=7mvM2;%)Ju3qh3W_p;=CI!A)BhEKkK>Q#n|xM8>8((( zi`)U_vI48C-bXzQJAzU`1dxm&2mzO{?_!#uJX=?0Z8k@0SayMF|9E}oFZ!bM^1x=J zp}j*gANEU~i_`7;Fffdi%WI=uw45s-VqMRr!P_vE{_s@*?mKBnPt!8 z0cV({`yM|%nJ!(krS@Q+`UZ3SqrvF}DJ13;m)qOy=g;0g9PXbM&kWt!Vf~8_#!Up) zYF~wxOCtppBGu)KWZYhB5bSnK79TP_?hn^fJ~RqzO;MW+SWSTiI7KajfPfGZ;$ywy z`M7``(~;n3fqEhej>yiQip}lNY^>xqA3ZFVBP?Zfe|-c455HLP`uh?RZRw*)fQ?N} z=l-^GQJn4b+E`;crE~V@?cLJKicXqf!1L+ac(k7R_sTpjU+qSzl25rRE+OH^%X1Qp zg+|kFe*4)H%S8s*2tJ?p#BIOSBpNTr?i04%o%0UOH4mB%?jU;EOg_vnXIyOe*DDho zq;x!VBqRo7f1HFvK77577TS1m#yuEPTkK{k9xfmhPOH4QnGfcQH(PFsx@pZfYDU^2 z=hoZZ$gR#VQ5h>!u45zmG(o! z!s4)>9d38iZnRnFcpOsjxVM2vLfXdaxbtDP>8(CYJzOYn98YWLA37MAGK9*#F+}zB zLnVDtq0)6bir;8;K6AWZ4NEG=EG%&wnf+$F(Wz7W$Wd`}@^HO!N0uhA<)zccRe#ZH zc@PJ6hdY(ZzSTQW_nIe#(e=&sQ0@>MoklbWB@XiDhn~Sfw6LQo<3M0x;ogiPbT}4c zgX;_qw`SWL^!@2dM}uX``pv;_5YJ=r4VLUjjWuuh-3fPr<{}>h!_&t%4|t?#8HyLT z(@l>D+YcW1>-{JXN3H{$udmP_F18BSM|DGEs&r=4IV1P{_Z$@)M)#|dGEYnHBOEGa z`gg3RG4N3>c2|)J363LZh3e1pvB>evKvW4-6YhAtXh9}iM<)D=$y;Ei@Ss3SDIyYh z6ET_b&Bk0m?EPZ@2YSR{ROWr}%TwXlK@H5?#>yZR?-%Wpw#H1TTf*SU3LR_t=^>xV z?DO?AXd^L)y-{A*X(~%gTyF&#neh=NanxcxQA+XRAM}6OZRQRB-Xp%wnkyYFmgtz? ztrYU#o**C_U(aV>Y;@)SzQP;&#>Ewd$2VDLfw#kYrayu>zC`wFm|rnZ#Jp5_l+j|Q{Y);OqD&PxN6HzKdYfUKTBVGR z|7ycW4)t33rh=TVOsljkCdS1~jjE`oC5;vHz0+o>8?2A^$14;r4zCHy{O8vEnzur! zWQ?L*0}fkle69+)xqWt2gV(1;q<)c7Rkz1O0x6-F$EsLjw#|2lLfNNBchVRL=54Hn zszo)ma;Nfp)Hu||D%~GvogC4)^)aZ={w$^L{yxH*&tm{`Us<8>V5!@r>&6z$h^Z{; zyuB&b^Ar;k4?J47z6e1sE26)-)TBR`-K@_i<02&)JG z2q_?SAd$g{CmP(UEBlm>u``IAP}(8I>f^2I2^WCXyuZ=vm6^?;JUsyF?JKmK*{^ z)ke!&Kb6g(#R&1f2!~B(dW`Nlkg?am>|}y`&iJcoSs-Pc*u}848$J1cZf!_^SS>Zf zWd+4zg1O!7)9QB4{`q}UO`-?_abf8Az!x6A^ywAI#?6S3gMxZOk$teNerw!KXCF+a zdtOfKg4zZ)T6|;?vH#$n^tK52_0qg?`MlpI?B-9(U`z_EMQ3wHDkM!$$BIgu7?x1+ z@I(^&F(?ls8p@pa|kN4=&OBrZ_#`)~GnZk)>{kZf5pa~`>G#96P zcZZTKm$$jA-r6h|%c=-A3!tM19a;Z#bZei8ELlY3ePg<(j)<|n(GKW*;< z*eQzgi0cCJ)T?H9W-`5g^Im1+Vl*NayoYJ-b=TeVuM?w{!4pmi>%u*DUBQY ztj@#X81Pc)j14Atf52&qi*hW9jjWrjw43z%%I05KtVca@Ez>*SxE_w@2w0fyHeo!^ zrb{ISHrsx&9}CGBO@N?XV9A+jB8fGJjbEa_GeNBuNRCD0w=o+Z*Q5GVM1r{-&ZDPc z&37xqYUqDK(Sk9kEBc-0N))F9a{|82!q1<*Js^Fpo6YCJ zWA%uP>%BofzFg<>cdPHk*@{2)-Cipzr>Vxr)|J-)vSUh3oGL*I1O_?`+L`NId(dvX zeY&NFF%;MvRoP{2dpKY7#=68j9~sAv=pPM1YxVtyj(TAPFrflOB#Lecm&nDDpr^{% zLlCehOzkLMS-=JV_`dg9C|6CubO^)ci60^zBlz~{>GP;%Dp6L}A_&-e3={nWU}-nS z+$D&}8DB;}|AT3~x$XIpo+uE3lal|!FDa0>6a1870d_V4F=78Dk;>G+R)G}@pNLha zKme9=6E4g8cV>O=;8k9%TE$K~t{P0y>o&Fp3&}>26Hl!~a)g2~5#7dL-YPH0~d7N6Q z*~CCvKUbm7>PgZ8C-pFo_Ut4hA}Y#EQb$Zk#vK+DlMS$;B=;5zWy~ZIPY)YD%xVE!^kPN? zy8%X&Sd2O%vx&RH{PX3iQ!6Xa08aA3$b$NTAYeEe`;(cPWy+<%8pnBpSY%~o85kI_7_X(3&{1YSL*dexQGbJ zSijkCivYNvR<)8YNeWwk-1GGjwfOpIuK4`?93a&qr@^0y(gp#t`~B@fHy(Ko*q3r- z)tztmn(nviIH8w<1_lO)M{+nVo=*;$Tu!l52ZXsc!$i531%4_&fBtlS8UwgrXGcem zk9XeM-{V%*4GrJrm{H-bpKeUkWClZbH>)?*aL44~8Op?-s~0vmH&3IQ?SraoYo$!P zSil^BEt3_2D9X$Q?n3wV$J4r$si`SWDLeG0-u3p_2b$}-e~Z9k zJfF_(1J*ay4^Z43qm)~m|5r*Cea zW>CLZ)%D(c&>IAQ^gF@?1`j22Ll~!1I(rLjn~7P%RioctljO#1c++LgnS|w^gvJn7H2Glksn~v$9ra3 zO((ngH`-hnD?hPY%nPW5QcES%!G2r&h9?k(>WFnLM0qAHT93voM8Bj-F9xmiEI8h;=?@m7dsFlfNczL;(I<2;iFgI zJnpSJ$AMXe0Xznb4Nk@ZB#byKz-{wUu|-KKu9yg*mQ+1}50i@G!3ouK_bPH04&z=>SSUWeDWH+U?D z$mgx2%YC@?jTT4BKcXs!)BybR*S;#>@$GJ{rdOUVRiaD=V=<+JVrjSC;EQy6dpgL* z{c7bq6&xM-ba%4YxGCKEm*BV&fEClFE+N@w?D zA=DsFkD4~T19CTI+C3hkOYz_`_}L6^4`+sA2*q)e8Fi7q__0e8dvC9|KsysQHt?d6*{vgqrGHpZ6!qsGZU?q5}KX zPwwF3hdv)K8};UMIhNI~Cau6Cni38{H)3I7LBoG@I!2$o29z{?zt^^A`)L%zZP#c( z8v9eBerU*9ERUCbs6cWB<`k?_qWb=PXNhN5!n9G-fPaJjb)<_N*&!gfbTL>O^%U$2 zplEGX6>$oN#?7FmO0Bgz*_#XUA1n^!ZEot-V;|pN?TCAR_qcon06+;Q7Joe=qL}pK zFix^>l);0(RkOSx!;Ii#8B^5rj1?Z0=XPifLacdyOJ!wcteWeK9WPO!XTM)QY04O* zc58o14>^vmJ<=wUKx15UFW-&>BBISwHF0*o`^!CjUhw_d-<`xwhp2qtfD&eP$b7$I z2(O@UEP4&}H^gK3q@d3N)SjN6sWk8X19%-Z8VwdL4tvj6LxeTo%o^o|0SU}LXh;+y z1kK_7dQW;QaBn~@034Jyr9?5^6XYF4{FU1qhy`abZ3h=lW{`g0LJry#jhqS;D4wN^v`X?GWo`y$bP?+E7am_Ih3Nb zx|#c6gF82Ceb31%{0(BoK666CWA@khvLYJ~_yxoJX4W?~$#saDCC8@vA_|HJicZG? zx@eIiN+NJVv+xs&1t4B!QO4+Tc@{w^h~XUYSxRY>|Ko}>%W2atSia$CjpQ;~mdzK1L~0<}&)96!^V`=Y zn1$NAacX0&FJL`-30jtFRZ-s?X9!QIZ`KMlNlM2wz}<~1T*%4JY3W&p@fXeG&*sgx zJM0Ak33xHY8sa%+jrxAR2%E7lSXQqAoom&|0N@00AqIu6Wx_F-RJCBnY-Th zRiOY>8zT0I0-{6=FW6deUX$%DLZskNYF;KdU76o`SK?(|{p25XT^SJ&yGcsY?IdL& z?g$wycwo%&IJC}FNB3lBKCwc_;WH2)ezZ5j30tlBrV&eqrJ{b5!1KIk*Zhu4i$QIY z*H?V$XkYaBTMxD1v&rSc1_Nx(SE)v2zi})X$QcwmL@4AwVY|R)9WQ#F>&sFbN^H+0 z-DFyrUlFw3zcrie=3^iC>Uub_JyX663g$*PgI^k=*ma{==N;tp_yauYO3%>TcR>^} zgEr3P-lLGF>_J8|r}rbGUBb>o05Sx9d*w9&VQz(lKQ5(1kGOFT!no+`9Dj|vh33ya zBVsd~7WivFmd($vBx>ER8Epe`@aNKpHe*3RZA%%hGA%ZK4hl*gAo9oiji=`3zD!Es zG>*=K`J9wJ|aNtO(0)&#R)h0>jEB=JMBPCo^pUz(u4 z0U+qJBs<=1{o;0rhy7FUzFMnbB|#WLfw_JIsH6{t<(jcQiRi@s<=z?rTaWKr4OFw; zrw4-5`u=|P1vH9^Cbohkgfm1hm&Mb(Pt+81-P}vGEX$$Asv<5le+k6Aolgd?NP`e6 z10eAgOMMuqGT^gV@9 zH@HWS4ms=_*`r?Uv>?J&p3v!VJh^n>Fw9OZXtT}v495ye9lT=L1+yR|7u6FSjKYtL z`P3I3K`98b*@l=Sa78cn5SQ4FSHN^)^? z67nf8^X)l{dhxRXtA;<%&Z|Br{hQ_x6Ac24p|>519R*YJIKPiP>FXZ2De8!IJ(#Ke zOrGe`U!*=Wq)LQX*o?2kJQ~sZJl?1l0TtbSfug+~f!X65$l%fgPEe#U&5Au;0vZI% zbM?sn07gQ&?Aw>e#Y3^GQLi(BiAi${KtSpFcq=9vv99r4h6RdUp@r@x_^@ zmInog$4Q-5O!A`_90ZW*Bgu&t9R#2p1!f_W;5_=hUg*M*)GibSLp~~=zlrN| zvA1U)B%Ci~4F~c|*eEv?!$er24>OPL#F4+qdxG%--a2oxxc#N{$zq2@+fJt z5)z^2U8JHS1fSK7-E|{t5}qk64Z+l*4Dlwko9(uM48iH8M&&+URw=hhhXO1tu|)5) zU;*+mz2rFXss^|`qKUu(F<#~+8AUOc>=1HHq$mJ|f{H3Mje+e~`sWWQ9wZk4M^%%>r9bJL&tXj#-ZrVkQTTJ9X-7{QSh)9uo4dd z&@pM1Cm;)pBNb=yA65=Wr_Q^{XN>|M4ImSAL-9v}|8^#61?1#6`xB8wEHQwkw$EK6`@NRi$JG# zIGiRn(Ezl1r=wX&Z|IMoEE!Io+eU!>ip76AX{Et(M=1}=Anl)TDIow|s{_`3+!eKEWNE=c>Q-@yT4**F-Lro3pej>eQBapL168$U$P!ZsA zLhI~p<#IYUiYN6dAd+H&3y}WI4~QV=z)V^Fhe2=8ceL_3pZF208UZR}9Gw_a!CzV= z1PTA~pr9A5RHuWBG&IuBh{g)`%W$nA-+ZQ)5{b4UbEjP2u06HS4N^=bAxyAovb#WP zHg%#xcBwT6J33x)8aus&p=gi=y+Zk=^d1fVThY&Q8uAZ#WMo^7;*&AxIGn}rR~VX! zX6B~u@>OS+&}0L@%Dfg7RaRJ&hB1Pq)zz1ACSutAH^jZ*5+!H9fR{ug=*lj+1RcLp z@Y~}ch#Hqs$I#5NDfN`TORESUTcj_X-K@vdHPlvat}8Q@1@@Lnr4~t%8ASSV=rM@G zH(<#&QY8_STZWPF`6!g*$?CSG?a@92oT{I`9CaWZh?tBbS1e*gC8uWHq0Qu8 zi_;JgPK}~p)K9dmY=`oyn~PE+yvRb1iZICtq>k_6RlH#yUy9RLm(Om*nJxJ?;KU*Q zH9NWx4Ze_5eWms*1RN$+(Sq!(Vk~}yrvWL(9oO!d?zkUn(ZEyJdWZY9ardb~<2Quc zFsUltK*F~-ujQ3V72E@Tk*`-5J)g2);=GDlzwOVS)?28={;u;Z_)+o3jJcGP6yej7YnYAM0=YgC=em;kspS+Jy3#F$v#~%GkLCmhn%J z31g`}J@!x~miv)f3=3bkg`%MZQCb222TI_%*bILWp6o>#uK+*xC9$rRU2F zKp+0Yd$)%ybpk!u+vZ=#f26U(?PZ&&%k@F$p7>%5XeSMWe%%o52B0j52If<(bjveY zcP-3R5dC}~{~RQ^FSrZW>W>Y=o4YrZw>D({2|fWH-3uZyH2Z;)_}?=TE$XBu0q-XpUN866hY^7EO{rF&?**y=HSyTDbpxCtUE2I z^F8?4&bPY`0Lvw3*v8(&Z6Yq))&M-$Br>FDX~sUA+(04v*C1O_3$jr-L$dClF@%lT>tHt4a^ z4E5p1@0F$(hOOTWM^(D`R9E--xTywaGVJEy)TCv?|{6{*+@yo>8KHNQW$!Sl*Ye{$fikAk z?WHVKlUZ)+*V_xC*X8ATYK?Gpy=j%R(=i)p+E(quPg}KWwcUG%hK;>T+RtXqyF_u> zaV63okmUi!WOW#IM>>UO%vSj?L;b~4f$j!zZZ5G|@bBN&>{A+~!7*c*+*60l0p}ZC z0pP{5Z*Nn}n&ab-H&ys;&bIg*CdaiHH^j@8l|g!F6Tcb;^RQVdB?=X>QoxzY)LH+j ztpJYQZ0Mr*xP9~nsKW+}2KidMEfRB8@O#1~kF%97z%^N0+gPc5l@10GjP;YAtjs&B zY(rVMOCMu0Cn15A|KPOc3wzn()zuy5s9BveA>joWzq2Nj?LfS-V`iqKjaHZD8xg?s zc)a!l%vO~3#<|C5sr0W^9nNpFA;^AvW2)!VmC_TxoD{^?WRcGKN$ZKNInOwd%08SA)Ew*x_u-P_eJOWO=N4_fU z@TSxuNc%z6^HD6rnO79QG?8_#fN8@2w774JRoB zf2xrFgonb216tcDTYZb*8$|@>l=lbUWjf6Qk>|8v?rx#7-&NRx?Fq>6LGlgUgmFf<@Nk& zdqUV{$X8G;llEP0{_wwCz%`SL!nJ-XU}%bk^m`H&6U(7JU-gC&FS5_y|IlQ+nMhZa zaq6G`cEw>kSsRW~)}7%^!2P)X_Ln~?c0be_`2ZL(m?CG+! zcN*u(&4Jv_zT6&1=KJ%L4rGmQQWmdEyzU!7Kr%Jj;(k@yz_x$*Z!n)Pyp3&luLxxnCh?_8RB63CL$A zstuNFF^JL*rm~|WB9s;e|CeRrvj-rpRH)UA9%g%wq_JYPT6PfzO@>0CYQ7;kFapq=tx@q4t-t_yqQj$(U_lZSI2!s zU0L46@ z_YjdBqK+FJ0n^FPVKZ?`Q^qQ7;Eg0*t~~+*&2!JI-mZ4%1*9n%8=bP%o6J)qVubr0 zPA1hM!G*Jw{l#U4ze4eh4`=%!$VpI9U0>3h~Qs|g%t>g4m6U`lj2Em6Jh zc0-5zQ`pBY>bVkg4UZ8tNhsfELxVg@KnBK|!Jd1EK0s%+SMVb)O-EVrg$Ye*9~T|t zEgh`^%}~y)a2<7ce{CFv?t>q+Hcq;%g*^p5!;-MQmR8Yf=~>UyDr90P`@i3e?PgDR zdE;^BIT*`nd5gcZ33%PzE@p#ARbnD>FuY?iW{KimT@?BNZ0XQ}NDd=s3TOU^#XEYW zTtB0;^L;vovVXSdn+doa1`EceJ}C%u{H0lkNO_} zR5~#v*}wJO9&L-eKzn3w468_%5T4NJGAEmdM{2dz3x9+qH?`y$j0b+)uQ==$GaesC zWj@7wZDrwWNObruq86pAq!8mKV<7b(0~*aB>0XwkaXbOVE|8eGD0AWG0?(e$B0ZCn zmFjbU>{b>I+AygKa^nxpiqI&UAtZDd)Y~!Dsk~9671nv46q~ln?~0*+Dtu;vk&6uw zKr1j$`0?twYYdoUBA28(suS{O$^B4>Tzd%PYhBJ)ms+iPosHgC_hfj|YT=K>4o*6V z{YzrytXTKvdh_D~B($M&3H=k@M&zQDNxrwCk*(Mr>m$EA79+ z23!zv((xQMIcK4fOCjTMUZ;%YqZAWeD>e!`9pP}Bz=7-B#RIa1DOWBVYR#17LQe&7 zgwo593i?XhhvRQvT~c8apdw-KcL`NPfWO}CNxRm6NM~aS{rVMi$t>mnEfocc#GU#* z9OG_!DYE`Vjxq}8YXGj>*$Vk7jKNCauQllmt~ZYBOaC2T6Lgvaga#%!h>IpQX5?M* z%MhqnuhUO+NixepxB9VeDB8GA59<@HPB<(oB%61*T^bF?g$x0PxEL5go&7NdzAj+z zld|9DD#_^R&<#z6(tH-s&J0ct1c?ITpg|)|i0MVHJ1JC_(Cn7;fFy7n+mj#1`&;Mt z*9_Fak9JD(R1`clSK4M4=1hql7l*W`z}y%?LOc1`2r-JI!;KevY*iN+NN+r*?d!dp zp=d(?rr`Ry({Blbk+|TCnYwV2gMKjZ$bE!t3j^f**MC9N{sd=lj@%~K`8*O7ke$f? z`}_f~g-Fa=trPw&e0cZN>1qq*aep;T25G^ek8T%Qe%&h(9@^uaX>^yv9*NNR!^y6c z$t1>N1W&j&i~-8{QMoKWsgFNSe<2UqXFV>%)8hIho!qLJjPUc2Z+szb)~|7&`LjEu zz!**!$`^HNjgSGza!7`1<_zCsHGdXlti^0pZd4rLO6Egz_ z0JEhW^Kj9;3hhtAOn^+5#(seHC@&AkNqWe~M39OorXOoMP(rAySp8*vXo^VVLNX}= zS#+xp(AaV$>u>-wU*zg6fWz1^12~$!*CwHn&z%7?3pbRl&v}Ni{~w|{wfYkAluG)) z4Yq;k539>#$^mCj#WGnhyGO#~B53fFWWh{!=+(qEcg2Jm8jlH4oz@KFg5KV9gB=oX zNCYeJ>I{)fQG^_bEoeozpyKU*xwNlEULk`KDU+p2Z9S>k#Z1OxE%Fij*100MUn`_O zik$uiHU8!ndKi)eO-ACSW+8>4oN@QRaX0FblvSS_`F<-i&9dBHJR$bn%;sC7QOhic=X! zR~7`ziR=LU)BCfj(rYj#z0-2)PvaSm0TYP$a;XM3zso-}&{)hs<;KJDBXy@Z)-h8I zOaG`yJ7iD-{_co3*4P&SoJ6~tt-Bmzhe4kO=flWum}_>8^i~CC7*pPxa>M0ng(Vq*zALL>sChY5l|p`%QGbq-=ec5DD$!$ zN?y&oB<8q&Zqki0)tN9D1oAOUfGNDe32TjUIi3EIRH_vGNJ_=kIJYRa{1WFijQ9Y} zEh1Gf#tanOI11cBP??y?6ZD4;k8_X0{S$LS@2Qi!Y>)$Jn27(YZvK0nlNYZ1cA;(>FfLjF!6BjpC z;vgnM6C)tQ9)-*RH3J~LU0)FDFO2@t_=prGGK*&kax`JjZ@EGY4MaDrL}X}b%}u;& z2b;TBEMlo#gl}DHYAnKig|Xy&TL|F~-U^EJ&UxyBGz$PXB@1Wp?8*;gKTd886`b@t zDT%(#lhYS9xTj8&_+Hjvz5Psq*%ZkXzDm~%q~Gd!i;Vb0bi!N%V&$=D965Vo0xArB zWhmgUV@{r=6{4hD*j5K1EZ2c5xXO7Zm!sov*!n_&p5d+ThlbOCm-M9(PL7O@13e?x zS}q$jvC~&J_P(qe3LGX>PnY2O_#n7qbK7|{)6!0K(xf!KU5hQU%|CxctH*< zUVv|->po4)tHOK!_TKZ+z_PR;rfjF?i#jW+QTE6*KaWVp5w)SRwliQ~2T>(amDf)6 z+@&A&Afy*J2{Yn9g33MZVWj)OTi9%We>XJOpta!N)R?j=Bb_AgY>$%GPDy@F!e?19 zpW|e8Lj2`I={2Buk*HioXOYy)+?EgXhJ*?GQ1O+eXqJFV?+=*bRY z@EFJ9)b`ZZ$1OL-o-r|gH_Sv|CtfbTy+5@7il7Q4jekz1q?0QPGGF$6c9YSOIfE%? z@t|1l^s>zDg#-{&h9_T2>jAlrE1NbX22R#eU+H^k_VeyZkyXkHLkW`VwQP}&COC`w zdarI%!cU-vHhu2TAB1htxq?U{2~`#Cw{a-(b?`xVMG^xDy0h!lYK0kmW6Ay@AlLVz z7BoUg;Gj8Xk_0MW(vYY_u~D(GOwG+*A5>8XBS&yCs3p(bK&V5m%p=l|(1ivOBGGLssyU<1Pl>&_!XgB?n{#^ovPBl9c3 z`uAO`uuT@13>q-HeFG$W=C`jLVcP={guL$PT)i+PtV?Y!wt&MSO;Q*xzCRM16)2Da zg!U$r2^=k6kl^9y`C1!Lsz`{9{d7{*#gY(=PKFIskajqq{Q?qgK=%gBPP8A-V|=hA z*g85oQMerU4-dfc@CcOo#Y!4eAX0}yJwIId0DdS-%M*ZDuh49YMit@Wbl8;uya(C* zK1it895xvp9lWd%5`y(CdfmS19JW%0igI#OfFFR>f4X-hf%0#IrOV6x8UOpEAy6R% z*u}=JBo@<~!Zv(9y!wI>0pkcZryUV#s0hGwKK!-qN}^IQSgQW#XEWG`rj~H;@e>lM z1H@RsM$^ZuAL!-?)HfK7YH4c&)aUW}`Q$i;B8vU65z8Z~-H%*Si4>;ZSD+jbs=jY# z2Gv^}sAo-c1Kf`App&3ztBp>4d&FQ{kUqgbkH(7MJg7eZKO8LN2#meGeW)eVHL}lc z2%)!L;L-KtmEkUTUS3`fzDoHIV-b=9B5ZOB3aIyC!1}Tk15_xbqZS1LcA3}coE)NE zZomXH5V{-dZ-g{6Gjl24-rk-=%Ik4&6nJ%Wyg+LflATTX8V!_JG6{VtAW9nn)SVns zUA<|k*{roP_qqzygP%2f zzi}BP15zvNwk6OV#@5!>r8uB_L*XB-G|-YF;ASxP(*cH>0Oxw!jV#oo0CX}-Sa|q= z=nNh=H2d#>Wd)?~N)!Tl1{B~j8DMgW8{}>b7|};ZM}MJ^5eQybT*N}X1H2I+eI}s7 zoZa#do6YLdz`)+?(+wFRP+yjZoROIcI5r*^b$vQqukrv52L)Zshy!@Bft>@8IA?of zDdvwdrhpGp94Jnp1Wstcl>_9!9JcGP-F^^}!$|LsJIH1sG8AoYHxzGd!~sBgQ959q z15`9OVB|3Xx%Cl1yY^o$V7Zn&*jp*(XuX4*lqGnuEs%KQ?sx$n5iw}P4}pV}1%X=P zK9*803ow^$8@PVFdqv`qL{ZXNELG>m#7yOhMu2i@yWXE^@pwann8aOwf@)U)FHWAg zg(+qM(XwQx=TnhfcBlDV5g;5in$PVI$C1jgdqP0JB*KY>Lcc>o&Ft*# z4ZH!sV*dizK`|q+0-|8ig))^O)r_rqz%pPDyiJi39DpL#?ywjBY`ML|a~%rXo;!}0Q?9SJT}Wmv79ZtY|f zz3^h8d~`1Dys3VBJidn;#9ZJYIwx& z(>nQ^>xk}<7~hp1OKK!;6BWcyCeka@=EM_PbEhMeirl}e%+KKx#}Qu>!$+`^Kmf}e zbC=L@gWhPhk`F_Q5Fow?evuglYE2apaiK!aqeS}cL)(xT>j^^Nv)p$XCSU8AkNF>) zFJEW0lT(v%px>6l0QD_t2dO&n&mC?>v@%wyt1w6rB42~7cgcvbeb3JpzSMFWQ^cbN z2lx7;laT^3{c|pN0nul{L*O*XinWvBZ<0pc-ULCWVEX~3YQq?19t^O;!abZ^^w_@t z{U;IQxN!d|Ov;kq7d$-2?cM!XcrwZZlfc?LokRAYIut^<>r~i(x{odudg2X-h{VGG z7~lCgBgpzMbClu*|9PO*%k+iHvVf1FGMl?2`KD8uu17Z{EG&8d!=D0_$KLMlcx`IF z520GC)%sDw;^7v=ROq@NZbUmz4b;12`I@7`U(I90k*G&B1y0bKW z;VzZY|9HKj33vYwu9S~qf1zvvV5If7dPTQ)c3uz<6)Q@HPSs&;{XpC{W@c)`tqp&- zMu0P7GsgNPo7dxXH>2*ygggH&wvnyv6qd)^itW+08#2MF6TN)3PKOx44mZ@+uGG2U zjsg{EXuA7vFWol0E903R^ONh|<7#`B#vb<;s>QMp<1sM@8Ac>t00~Y3UAWX#^=5 zhDK6KN=i_2CGf-zV7?FK99q4WoFFQjAL=#1^faug*?_C_8Nk!WqZ0wd)si=q_tp)*N!yW`Y-|- zOTv~s@c;A)!*5}_FyBo3y9FtpUixk3fssPdY*>svvrL<`*MZD%;5O#zVyM-G@I-}? zbEc?cRQy;>ULR2~QYckX*xI&}V3kkM{J?ak+CQPe_ycbh`T+65SjZLoj`vT=n;qZo{7=&?!dWI6M9IOXxa?8AVPHQVBt(N;xiDr`oK?d&$e9 z1AZBP^l_Q`5Tw)`bYG-Md91aRgmL=vc@1_?{assF&s(2063k75EFCqEUaa8B#>55v zy&57WvFI|KPAsgYt~#W7(v)M`b4U5^4G=(c1i6EhJ=W3q5msM!*g)Q>c~RaUd(x{w zXH?sZ&E+D$7UJKsX=T2TK!XePCO-!<9TWQJ_)2_x|JF^Pif>Sb%U5>LY&qnH2B0@zBr>7B~ z^SjjH?o#_cx?!O%8Y93zgQNa)(Z|Wzd40sZT27x=tpzV4JUl-zV4-&Nr>tp~gtGxl zC*RYx!JJY^*x)$jkwG>?Ds`aW8AGBaTW70orrDil;a`lPaeefQJ5kJ%cXzyl5XbZ6 zfudd^>NPRdG3thFk7$(YZd zp!>*@@R{-#;>+n>18W?^y#ShQ zYz>Wyj0_s9%Wb0$YkH5}`I1*5t(G4HOxk679fiEHc2$cE+@9-j6$%BU&d$#A83{g@ zrkkH1i`)sPga*1<2YjNyOBeG!Jb zAE0zKL8ndsZaRBW6!H1mNX61j%??OZg^SkWVP$U=4yBFt;Su-3B%I7rK~7DQ&~C^u zNYD+5B6~|W_^rvlVmt`Jy9vlBhsmY!lMb37NyS3ovHq}k_#@h7d2dm^G$CK%!F3E< zoFOYc0i#p^QmEHcXT6ZKLHH{<6B89}S=Y??bO5M>D0?>W?J5OC)caoYk`pwnYxy3a ztiX!;M(o*Koy~i&qJg!=YwwSu!VH8vL~FLp2HmusHdARrJvaD}9{qFfci)eKb|KH; z^VMV6m*Yif0mKDkT1!qI#1ymh6oesx`T5m}oqGD`PlLb{kbXA{SDaSNjUy@le1!2G z^W30-=}fAi(X*5KC2@&xkcoocqZyG!kEuCPKK_yLxj;GYzW*8VvJIaKu8MQj8wtD1 zH%*{E*^Z6ftSzwNHTu&ATTzeDdUy%uN=O|CNBP9v@vQep-CM%n!PuME@*Y6;M3G$9 z^+~ibeaN=xr~-+g4g6TTNs4eH(x_}4Ptlt^ksS#M0i<`X(r-H48y#2(z>t`rD7TzW z{HU2hSIlx6f^BeIhdYwfR6OIEA~P~~nB z#HUE2nAHtocJOS6YD=Q(@Zf5DZlaQHt`Q5;385RrvPGKOp3FvKV%)i8IK5{x6M#$f z6r_hJ!Vgv4o*d?Kx@s7Cy!(y_Bbyt!&`=;5jcEM2u2)=%#qJqK39V^EW857cJ}|39 zAd+Hc5^k&g!Mk=BW=PP|?T%oxOOjXx0{C_xn{trsSg_tlCaN?h<`7*@a=}AI5F6kP)-|i!?L%xFTkIO zrA)=YD&u&@mn%7=W-4D5Zl4yM9t)BOKmztbcnt#7CaIK?7z$SIyrEihHAv<$P;Dne zG@6a7aS%*J<6{{=M{=n$$`@2WZX@2e=2^~j%wd5?v6Uu?h&{}$iS#OS&8EGAAK;n4 z(=Q#JYp@S{F-Z25Fs_HRm6VxTu#M-nkbOTMo_WH6Ehxm;u~|J?0B!{p@`Lq?t?_SI z8wyO_laongK%`#fj`hMgY%^((r?fA@D+w2S`&>oLw~7kcfAj&bl0*MmHY!>HOU zmIdq@;{!?xI=aEXc^jcZ|3kbD|Ax`wMi3SZv^b5>lTSdj zNWM-US_K&ekm(_hy2&#|!KEdHFiR8)R%HfcqL|p&^IywYC@jmR&Ol~T(re;Yp|rs0 zQJlWl0b*>4Y+G7|fG_rO3Ika)2|uHZny0D9#;uu=7gQ-Z{aMY}K9>>y^uY&MJOT32{! zMidisyDkkHtojPZpfSb+5oSq2rEsNb1fD>Dng4#w&>8C*3t=_t=ghvj^u>XxliTZ6 zBRW$5Lc=gfzg7$#RztC?&!HJ;w=`APsAp`fW)`#1xB+}#`#=m!of&qV9?HqRuP_*u znRfT_6)?pv-CEhWwQ0hqw7&kaO)^C`er3|Y|2~y1)8^dGbmOFwvq+vqRmx@7edZ-) zlLd8O?Yq5riQu;c0*8;{;#0k7O|!|uN0z-hCK98sH+-XI#Q`Z~)^&_8^KT^(grbDs zE6#upGMb^&$gEkg2Ow}JWfYIo2p)TZgVY(irVxBVFMY{hiVX$X8~3*iwR!(F}DzcJKv)OOKdhHj9X;0er;vt!r5jFj05V2-}?96S!j0c z4xPD_jmt8$Zx!)6HEz^7Ry1fo^)zm?)v~qa7`ib&e`n;m5aJpOb2pxw_vlRO>IF0#@N%_F10Oq1Dxcrzn~R}jE|Q_t zivIdo&|s+5geX|WzT}P;rUd#0_%S}%tUt8^JO;;a&6lUQD!vwF{EGaQT0xE8nW{F( z@|BOLJxAAtz?aqZl~=Tl(LkUXgXV=`#_Pz)*?O;U4;Fq7mbmFP1wuqP}9Y6O@&BBJ4fHsog`1wF1brKeI%knP(du(bGK1>-VLkRG^_XkK2uTQ;C_x)NQOlQ_oIg2H$c*N;^-= zu-=wdGf&FTdb!YKw_{tzbyUQ%GaU>`2=L(d+>a%l7mm<@A!9N=2gu)5m@F7YIOx+| zSw!4Fz>nT=f&y+N_=^ExMWGbf`njxoLoQNh?J`9ATkD^) zH4}<2Aps0gpv=wrQr$d+Zm0X|YF4a@s32mJZ4F#myX{PH14b51=DLR&&6VE&y`jdb zk50>-TCA`DE2P`XzCpXb}VF1ZSH%?zS#Q~{OAJ}w{Zd74letbE?Uht z(a5(xzVEWr_)~VFF!`R-_)R~Mjk_{RJPF8}W&}p~!%g`B(>;t!vIoiibp*!Uc$}dnU~&H_ z>h{U)C9NhcVi5#;7zCMF0xxd>D^%@W%3}D5dgNCsf!td@Ww5KX0VkT2 z%c2-4E(3@QOYO(<-f6_Te*@xZXR!>|I&R45E(;X#8YFo=f5jIa&#Lg#Rz?TYFoNha z|C2+}r&=Js++AWVflai2n8nM(&7z}iZ-V)oUJVTmAsTv?t1!G4D%Fbw?C`}-qLJklK(io{gy4d!)DV^Rpg>I+Wu zfgv7;{_gH>jd|xyLH46tbOlxgAZF<>RnK@TdY|L6tv%0%=tfK>e-#8GPsq8{=F_A7 z2^JY0J($eJjJaLi4c476E?ABDuX_(lI@d?CwEa6GRMcW`;)}d`^Vc}uL(M#-!DAG7 z(lD*=R#}FOq$b=mUV^Z#!==bJjwo$S1unry{c&aCaS3GthQnBD%;#1Vfz0~E7ocqD z>F#FPK^p8F@mc;6Z!}1?pcE_QqoW%cQKu|qv_bJdpK%G2#Lgr};py==B|JW_bG{g! zqnH?w_b2FmFhICx4==CR%sc>IoHdVXnr?-pB+H(`!NHNLjeUxUi=}p@oPPg886>;& ziI$npCH?@}w>YQ)9;a0G8;&Z`16=U44SbwD|@D zcPz8$l+I6pc@TGb2bdym>7-(;xs!&s6TQGC&6$1B-mS?2oJy}ub%9PS`<>&a{dCNX zf%4D_rHuvnPVlk3g>WLKZ_N)?_@6IT6ZZOFQ6$Vh@2^A%K@pEwiR4NjX1dxqe|c}Y zZs9*)H2(@q<%c6sK0+JGzF=}<87^V9VLLp_ieR>lDNAaJbObidg#vwjq7k+S8PfK& zdYyFW;FXp-o3{{t@14c&M_n_3Y~k$egiEqJRq9#_2te`7_u`n53c^IH|7ZNIaOpKX zrZ3c~x#_ML5o^&I!1>1kP=K&bZ&CbF%>g5DYtt1jqQK4H5K{x_w1VsRQOI=ce)TMi z(Q3FQ^Dzt2tNeR_5&^%WM64U%1T$GYaqe4emeClmN5&;p%)o<<3_4cn-HL3IZnbC9=I)85KX>9v7^$wip4vBy5_ zu=#q?vIaD^JJ_8SnSYj3Pb!T&Aicz`OoK1D7hy6d1fc{)h-!$|3FO8KD2>|uXw9pe zDcHLofL~}yt*@C7S^x9lJ2Tb*g|LIQjm?W;^j-6j0z(b+Tr$^Dz;~$6URV#VW+tht zwKbHWg4OZFOZ0J`?dOE9pa@o_bTO&aeVo~Mw2p`!eYW&_Ib)&j11cP0w<$DW=gFq} z!_;Yo-XHGx=?F{;iHwvzt}zSIkYe`G-=XV#!OfG-*C@{4GinWCTSh&nUyIk?diu^# zw^@<9CV=}mt=NU6wmGT0x8J%DRZ<}>_Vev>*#va{3X>-K@dhLWX;au_5{NWXL+UGi zz0FZ4n(xOgtzKTuulomni~gaBk2Q6Y>Uf+!wP9MiW4US*_Z)ARLKlctLcZ%xJywd& zYr1o2EN)Kv=XF^CZ%UzPMd0nXCyI$~4?@GpZ+)x@7*9&q=Km!4^&Lxd5`)&{nm>UM zgzG;DFlQ(3eTSdp4tEO(5sqefqJVZdj-9EgOaKuAItlkj5HQRtgDz7ju@0W%e~LCl zB9WjON&7cc@c$E2_C z!wY#3{)1}6hoV*T;Uuz7|%MnYxSTpHN}Y8jBGq(_4Twu71E_|%My^e3E%(dp6Y zY2~{{bfoFVAv&7IWUIQ$?Gi_)so2E?K5dJ4B1%FUloCa}Q|VD))(Idb!HaVAaE$dv zx{#D_Gjvc*fRmgD6|CQ%oa2aHy7QeNb6%7CRWMBND6`x|)A^2|C_bSS&Y6Nzc0O5@ z=`wYhHis5hjpYaISkfYadP!ROrD=u7lV3Vq7T~c$FI5#Zqa)JgET!lesE3>w1b(+l zORtV}#l{I*>An(#SVL?$$PGK16?43c*>S)Gd$ZR0wd*WbW^#r3^1*X%!U01}ZWlB~gg(=n=~yQ$G6))2+N;gIwB`H-fiG7XsY_YRuo z^#ceH=fk^RZ*Z8z2?Frbzul)8#tV5x*=;OUCpA7jx07_^^@+#geMZDzTe~r=!-#0khP+n|XK{qJ2FbdF1$_=Fda=fU1uqS~-$uevcj*&rKfDecc?W zCtAOubr~;rB7^@zaQhQz@xn=0R=P^GC0$KC)8A{}F@5hdtI;W)Gk+=K-CfP_=YwJO z9y8@RRZKBeny&6LF2-WAxP-DQqv@sUqAs@k=_nzU`cfXmA&0K@W7b5dR4er~D zfr54>QQg6QKJvk2IM;Y0_xG0{Rze5sD!b4}?Ia2T-=xQQ&Fj;O6n{1gD9E zx{#nknicl@dUq0SQ-hD&TLn`er@5g~oe&>xfTJUffJIm8CA{0flcb}GG04mOELS?;EbeqVlwK?I zfsoL$s?y1XswhDe0Z6dIa!xxO2mIR)h^5?%FzCHZ{zTS9IOn|5F91*!fX78e*_uR| zRo6D!h61dsH5_%<;bbYN@?e>eikp_6esyOjiOUEvR1z$IQgOyYH?V2;W1t~$)m^Ga zTszm&+9XQGB8~mMILIjt7Sl1OZm%cV&s~sm%9N+x)!nWM`7P8>PDbl~a@t`zbJPx7 zSDkk>;GN-MJNVtzna=ipYuDp!@W>w=rM?F8)Mwl?EI_y(Kez*V-qr3$m7)|+xwYgt zH{KofwAY3#Go` z`_v47Yfb0E7RK8U?Cu1-fE$HZ?-9UPR36AJsZk8JE!obRAo0MqF!LV~KKV3v*kN&X zf{Kb2O3n5sp8DFq&}^yH^S`(=3}?R;yS<#j=tRPrlW%(HrK^YPvZ{^Dg-)ME-pO6g`i^x zzsVO6|2f_TZrNcX?2IkvA5aPd$&=8yxHWXYDhJi5R^~}}Lv3yTOOwi8r9?}_&Mt%g z*t(>e*7~_7VZ7Ff;j>h zYu2bKA#GN$+DDKuX)9*7B8ZKe9MLU3(v_zhg~+aef(`TGuRi`5#rODO0F|XfX+uy+ zyOOi;S-8{B*pV`Zg5cPe*J0Xu@xe%#KnE;&|m4T+ZlFl8FvE2{L-O4o}~|1`dg zoFcETdY{UjxV=_^nl#G}`CF2uLb#jvr6`KtQ8l?RoPkZ7=uzGI_V}djb zJ4knTKkw-qK Date: Tue, 4 Nov 2025 15:07:07 -0500 Subject: [PATCH 2/2] Adding readme.md --- .../langgraph-js-elasticsearch/README.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 supporting-blog-content/langgraph-js-elasticsearch/README.md diff --git a/supporting-blog-content/langgraph-js-elasticsearch/README.md b/supporting-blog-content/langgraph-js-elasticsearch/README.md new file mode 100644 index 00000000..619b0ea7 --- /dev/null +++ b/supporting-blog-content/langgraph-js-elasticsearch/README.md @@ -0,0 +1,43 @@ +# LangGraph.js + Elasticsearch + +This application was developed to demonstrate the integration of LangGraph.js with Elasticsearch for advanced search workflows, as part of the Search Labs blog post [LangGraph JS + Elasticsearch +](https://www.elastic.co/search-labs/blog/langgraph-js-elasticsearch). + +**Practical example:** A smart startup search system that combines LangGraph.js workflow orchestration with Elasticsearch's hybrid search capabilities to find venture capital opportunities using natural language queries. + +## Prerequisites + +- Node.js 18+ +- Elasticsearch 9.x running locally or remotely +- OpenAI API key + +## Setup + +### Environment Variables + +- `OPENAI_API_KEY` - Your OpenAI API key +- `ELASTICSEARCH_URL` - Elasticsearch endpoint +- `ELASTICSEARCH_API_KEY` - Elasticsearch API key + +1. Install dependencies: +```bash +npm install +``` + +2. Run the application: +```bash +npm start +``` + + +## Example usage queries + +- **Market focused:** +``` +Find fintech and healthcare startups in San Francisco, New York, or Boston +``` + +- **Invest focused:** +``` +Find startups with Series A or Series B funding between $8M-$25M and monthly revenue above $500K +``` \ No newline at end of file