This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch financial-analyzer in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git
commit 619124ac36aa18090443889771b6f2eabf7a8e19 Author: Andrea Cosentino <[email protected]> AuthorDate: Tue Oct 14 18:09:53 2025 +0200 Financial Analyzer example Signed-off-by: Andrea Cosentino <[email protected]> --- docling-langchain4j-rag/README.adoc | 1 - financial-doc-analyzer/README.adoc | 288 +++++++++++ .../application.properties | 41 +- .../examples/banking-sector-brief.pdf | Bin 0 -> 33526 bytes .../examples/magnificent-seven-update.pdf | Bin 0 -> 35941 bytes .../examples/semiconductor-sector-analysis.pdf | Bin 0 -> 30155 bytes financial-doc-analyzer/examples/tesla-q3-2024.pdf | Bin 0 -> 27956 bytes financial-doc-analyzer/financial-doc-analyzer.yaml | 540 +++++++++++++++++++++ 8 files changed, 857 insertions(+), 13 deletions(-) diff --git a/docling-langchain4j-rag/README.adoc b/docling-langchain4j-rag/README.adoc index 326b535..3da4c9b 100644 --- a/docling-langchain4j-rag/README.adoc +++ b/docling-langchain4j-rag/README.adoc @@ -53,7 +53,6 @@ docling-langchain4j-rag/ ├── docling-langchain4j-rag.yaml # Main YAML configuration ├── application.properties # Configuration settings ├── compose.yaml # Docker Compose for services -├── run.sh # Convenience run script ├── sample.md # Sample document (copy to documents/ for testing) ├── README.adoc # This file ├── documents/ # Input directory (files auto-deleted after processing) diff --git a/financial-doc-analyzer/README.adoc b/financial-doc-analyzer/README.adoc new file mode 100644 index 0000000..0a0a739 --- /dev/null +++ b/financial-doc-analyzer/README.adoc @@ -0,0 +1,288 @@ += Financial Document Analyzer with Market Intelligence + +An automated financial document analysis system that combines Apache Camel, Docling document conversion, LangChain4j AI analysis, and MarketMind REST API for real-time market data. + +== Overview + +This application automatically processes financial documents (earnings reports, SEC filings, analyst reports) and generates comprehensive investment analysis enriched with real-time stock market data. + +**Key Features:** + +* 📄 Multi-format document processing (PDF, DOCX, HTML, Markdown) +* 🔄 AI-powered document conversion via Docling (async mode for large PDFs) +* 🤖 Financial analysis using local LLMs (Ollama) +* 📊 Real-time market data from MarketMind REST API (Yahoo Finance) +* 🎯 Automated stock symbol extraction +* 📈 Analyst ratings and price targets +* 💾 Markdown analysis reports + +== Prerequisites + +* **JBang** - https://www.jbang.dev +* **Java 11+** +* **Python 3** - For MarketMind API +* **MarketMind** - Clone from https://github.com/oscerd/MarketMind + +== Quick Start + +=== 1. Start MarketMind API + +[source,bash] +---- +cd /path/to/MarketMind +python3 api.py +---- + +Verify: `curl http://localhost:8000/health` + +=== 2. Start Infrastructure Services + +[source,bash] +---- +# Start Docling-Serve (document conversion) +camel infra run docling + +# Start Ollama (LLM inference) +camel infra run ollama +---- + +=== 3. Run the Application + +[source,bash] +---- +camel run + --dep=camel:docling \ + --dep=camel:langchain4j-chat \ + --dep=camel:platform-http \ + --dep=camel:http \ + --dep=dev.langchain4j:langchain4j:1.6.0 \ + --dep=dev.langchain4j:langchain4j-ollama:1.6.0 \ + financial-doc-analyzer.yaml +---- + +=== 4. Process Documents + +[source,bash] +---- +# Copy a sample document to process +cp examples/tesla-q3-2024.pdf documents/ + +# Watch the logs for analyst ratings +# Analysis report will be saved to output/ +---- + +== How It Works + +1. **Document Detection** - File watcher monitors `documents/` directory +2. **Conversion** - Docling converts PDF/DOCX to Markdown (async mode for large files) +3. **Symbol Extraction** - Regex patterns identify stock symbols (AAPL, TSLA, etc.) +4. **Market Data** - Fetches real-time quotes and analyst ratings from MarketMind API +5. **AI Analysis** - LLM analyzes document with market context +6. **Report Generation** - Saves comprehensive analysis to `output/` directory +7. **Cleanup** - Successfully processed files are deleted (failed → `.error/`) + +== Analyst Ratings Output + +The application logs analyst ratings to console: + +[source,text] +---- +================================================================================ +📊 ANALYST RATINGS SUMMARY +================================================================================ + +✅ TSLA + TSLA is currently trading at $439.27 (+10.52, +2.45%). Analysts have a + consensus rating of 'hold' with a target price of $366.77, indicating + -16.5% potential from current levels. + +✅ GM + GM is currently trading at $58.37 (+1.03, +1.80%). Analysts have a + consensus rating of 'buy' with a target price of $64.48, indicating + +10.5% potential from current levels. + +================================================================================ +---- + +== Configuration + +Edit `application.properties`: + +[source,properties] +---- +# Directories +documents.directory=documents +output.directory=output + +# Services +docling.serve.url=http://localhost:5001 +ollama.base.url=http://localhost:11434 +ollama.model.name=granite4:tiny-h +marketmind.api.url=http://localhost:8000 + +# HTTP Server +camel.server.port=8080 +---- + +=== Available LLM Models + +Change `ollama.model.name` to: + +* `granite4:tiny-h` - Fast, recommended (default) +* `llama3.2` - Higher quality, slower +* `llama3.2:1b` - Fastest, lower quality +* `mistral` - Alternative high-quality model + +Pull new models: `docker exec -it ollama ollama pull <model-name>` + +== Example Documents + +The `examples/` directory contains sample financial documents: + +* `tesla-q3-2024.pdf` - Tesla Q3 earnings (TSLA, GM, F, RIVN, LCID) +* `semiconductor-sector-analysis.pdf` - Chip stocks (NVDA, AMD, INTC, QCOM, AVGO) +* `magnificent-seven-update.pdf` - Big tech (AAPL, MSFT, GOOGL, AMZN, NVDA, META, TSLA) +* `banking-sector-brief.pdf` - Banks (JPM, BAC, WFC, C) + +See `examples/DOWNLOAD-REAL-PDFS.md` for instructions on downloading real earnings reports from company investor relations pages. + +== API Endpoints + +The application exposes REST endpoints: + +* `GET /api/health` - Health check +* `GET /api/market-data/{symbol}` - Market data for specific symbol + +[source,bash] +---- +curl http://localhost:8080/api/health +curl http://localhost:8080/api/market-data/AAPL +---- + +== Troubleshooting + +=== No Analyst Ratings Found + +Check MarketMind API is running: +[source,bash] +---- +curl http://localhost:8000/quote/AAPL +---- + +Verify `application.properties` has correct URL: +[source,properties] +---- +marketmind.api.url=http://localhost:8000 +---- + +=== Document Processing Timeout + +Large PDFs (>50 pages) may take longer to process. The Docling component uses async mode with a 5-minute timeout (300 seconds) and polls every 2 seconds. Failed files are moved to `documents/.error/`. + +**Async Settings:** +* `useAsyncMode: true` - Asynchronous conversion enabled +* `asyncTimeout: 300000` - 5 minute max wait time +* `asyncPollInterval: 2000` - Check status every 2 seconds + +**Solutions:** +* Async mode is already enabled (better for large PDFs) +* Use smaller documents (earnings press releases instead of full 10-K reports) +* Increase `asyncTimeout` in the YAML if needed for very large files + +=== AI Analysis Timeout + +If you see `dev.langchain4j.exception.TimeoutException: request timed out`: + +**Current Settings:** +* LLM timeout: 600 seconds (10 minutes) - configured in `financial-doc-analyzer.yaml:34` +* This should be sufficient for most documents + +**Solutions:** +* Verify Ollama is running: `docker ps | grep ollama` or `curl http://localhost:11434` +* Use a faster model: Change `ollama.model.name` to `llama3.2:1b` in `application.properties` +* Reduce document size: Use press releases instead of full 10-K reports +* Increase timeout: Edit line 34 in YAML: `.timeout(ofSeconds(900))` for 15 minutes + +**Recommended Fast Models:** +* `llama3.2:1b` - Fastest (1 billion parameters) +* `granite4:tiny-h` - Very fast (1.5B parameters) +* `orca-mini` - Good balance + +=== Empty Market Intelligence + +Check logs for: +* Symbol extraction: `🔍 Extracting stock symbols` +* Market data fetching: `Fetching data for symbol: AAPL` +* API responses: Look for HTTP 200 responses + +== Cleanup + +[source,bash] +---- +# Stop Camel application (Ctrl+C) + +# Stop infrastructure services +camel infra stop docling +camel infra stop ollama + +# Stop MarketMind API +pkill -f "python.*api.py" +---- + +== Architecture + +[source,text] +---- +┌─────────────┐ +│ Documents │ (PDF, DOCX, MD) +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ Docling │ Convert to Markdown +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ Symbol │ Regex extraction (AAPL, TSLA, ...) +│ Extraction │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ MarketMind │ Fetch quotes & analyst ratings +│ REST API │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ Ollama LLM │ Financial analysis with context +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ Report │ Markdown with analyst ratings +└─────────────┘ +---- + +== Project Files + +[source,text] +---- +financial-doc-analyzer/ +├── financial-doc-analyzer.yaml # Camel routes (3 routes) +├── application.properties # Configuration +├── README.adoc # This file +├── documents/ # Drop documents here +├── documents/.error/ # Failed documents +├── output/ # Analysis reports +└── examples/ # Sample documents (MD + PDF) +---- + +== Contributing + +For issues or questions: +* Apache Camel Community: https://camel.apache.org/community/support/ +* Contribute: https://camel.apache.org/community/contributing/ + +Happy analyzing! 🐪 diff --git a/docling-langchain4j-rag/run.sh b/financial-doc-analyzer/application.properties old mode 100755 new mode 100644 similarity index 52% rename from docling-langchain4j-rag/run.sh rename to financial-doc-analyzer/application.properties index 1c43df8..2f6ee6a --- a/docling-langchain4j-rag/run.sh +++ b/financial-doc-analyzer/application.properties @@ -1,4 +1,3 @@ -#!/bin/bash # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. @@ -14,14 +13,32 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Run the Docling + LangChain4j RAG example - -jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel run \ - --fresh \ - --dep=camel:docling \ - --dep=camel:langchain4j-chat \ - --dep=camel:platform-http \ - --dep=dev.langchain4j:langchain4j:1.6.0 \ - --dep=dev.langchain4j:langchain4j-ollama:1.6.0 \ - --properties=application.properties \ - docling-langchain4j-rag.yaml +# Camel Application Configuration + +# Application name +camel.main.name=FinancialDocumentAnalyzer + +# Directories +documents.directory=documents +output.directory=output + +# Docling-Serve Configuration +docling.serve.url=http://localhost:5001 + +# Ollama LLM Configuration +ollama.base.url=http://localhost:11434 +# Recommended models: +# - llama3.2 (default, good balance) +# - llama3.2:1b (faster, smaller) +# - mistral (alternative high-quality) +# - phi3 (Microsoft's efficient model) +ollama.model.name=granite4:tiny-h + +# MarketMind REST API Configuration +marketmind.api.url=localhost:8000 + +# HTTP Server Configuration +camel.server.port=8080 + +# Camel Main Configuration +camel.main.routes-include-pattern=financial-doc-analyzer.yaml diff --git a/financial-doc-analyzer/examples/banking-sector-brief.pdf b/financial-doc-analyzer/examples/banking-sector-brief.pdf new file mode 100644 index 0000000..5d01ed4 Binary files /dev/null and b/financial-doc-analyzer/examples/banking-sector-brief.pdf differ diff --git a/financial-doc-analyzer/examples/magnificent-seven-update.pdf b/financial-doc-analyzer/examples/magnificent-seven-update.pdf new file mode 100644 index 0000000..a7c9aa8 Binary files /dev/null and b/financial-doc-analyzer/examples/magnificent-seven-update.pdf differ diff --git a/financial-doc-analyzer/examples/semiconductor-sector-analysis.pdf b/financial-doc-analyzer/examples/semiconductor-sector-analysis.pdf new file mode 100644 index 0000000..037b8f0 Binary files /dev/null and b/financial-doc-analyzer/examples/semiconductor-sector-analysis.pdf differ diff --git a/financial-doc-analyzer/examples/tesla-q3-2024.pdf b/financial-doc-analyzer/examples/tesla-q3-2024.pdf new file mode 100644 index 0000000..9237da4 Binary files /dev/null and b/financial-doc-analyzer/examples/tesla-q3-2024.pdf differ diff --git a/financial-doc-analyzer/financial-doc-analyzer.yaml b/financial-doc-analyzer/financial-doc-analyzer.yaml new file mode 100644 index 0000000..55c1b8b --- /dev/null +++ b/financial-doc-analyzer/financial-doc-analyzer.yaml @@ -0,0 +1,540 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Financial Document Analysis with Market Intelligence +# Combines: Docling (document conversion) + LangChain4j (AI analysis) + Market Data APIs +# Use Case: Automated analysis of financial documents with real-time market intelligence + +# Bean Definitions +- beans: + # Configure Ollama Chat Model for document analysis + - name: chatModel + type: "#class:dev.langchain4j.model.ollama.OllamaChatModel" + scriptLanguage: groovy + script: | + import dev.langchain4j.model.ollama.OllamaChatModel + import static java.time.Duration.ofSeconds + + return OllamaChatModel.builder() + .baseUrl("{{ollama.base.url}}") + .modelName("{{ollama.model.name}}") + .temperature(0.2) + .timeout(ofSeconds(600)) + .build() + +# Route Definitions + +# Route 1: Financial Document Analysis Pipeline +- route: + id: financial-document-analyzer + from: + uri: file:{{documents.directory}} + parameters: + include: ".*\\.(pdf|docx|html|md)" + delete: true + moveFailed: .error + idempotent: true + idempotentKey: "${file:name}-${file:modified}" + steps: + - log: "📄 Processing financial document: ${header.CamelFileName}" + - setProperty: + name: originalFileName + simple: "${header.CamelFileName}" + + # Convert GenericFile to file path + - setBody: + simple: "${body.file.absolutePath}" + + # Step 1: Convert document to Markdown using Docling (async mode for large PDFs) + - log: "🔄 Converting document to Markdown with Docling-Serve (async mode)..." + - to: + uri: docling:CONVERT_TO_MARKDOWN + parameters: + useDoclingServe: true + doclingServeUrl: "{{docling.serve.url}}" + contentInBody: true + useAsyncMode: true + asyncTimeout: 300000 + asyncPollInterval: 2000 + - log: "✅ Document converted to Markdown" + + # Step 2: Store converted markdown + - setProperty: + name: documentContent + simple: "${body}" + + # Step 3: Extract stock symbols using simple regex (faster than AI) + - log: "🔍 Extracting stock symbols from document..." + - script: + groovy: | + import java.util.regex.Pattern + import java.util.regex.Matcher + + def content = exchange.getProperty("documentContent", String.class) + + // Pattern to match stock symbols (2-5 uppercase letters, possibly followed by stock references) + // Looks for patterns like: AAPL, (NASDAQ: GOOGL), Ticker: MSFT, etc. + def patterns = [ + /\b([A-Z]{2,5})\b(?=\s*\(|\s*-|\s*stock|\s*shares|\s*Inc|\s*Corp)/, // AAPL (company + /(?i)(?:NYSE|NASDAQ|ticker|symbol):\s*([A-Z]{2,5})\b/, // NYSE: AAPL + /\(([A-Z]{2,5})\)/, // (AAPL) + /\b([A-Z]{2,5})\b(?=.*(?:stock|shares|trading|market))/ // Context-based + ] + + def symbols = new LinkedHashSet() + + patterns.each { patternStr -> + def pattern = Pattern.compile(patternStr) + def matcher = pattern.matcher(content) + while (matcher.find()) { + def symbol = matcher.group(1) + // Filter out common false positives (financial terms, acronyms, etc.) + def falsePositives = ["CEO", "CFO", "USA", "USD", "INC", "LLC", "LTD", "PDF", "HTML", "HTTP", + "ASP", "EPS", "YOY", "QOQ", "TTM", "EBIT", "ARPU", "BUY", "SELL", "HOLD", + "TV", "FX", "EU", "EV", "AI", "AWS", "API", "APP", "MAC", "IPAD", "GPU", "CPU", "US", "FSD"] + if (symbol && !falsePositives.contains(symbol)) { + symbols.add(symbol) + } + } + } + + def result = symbols.isEmpty() ? "NONE" : symbols.join(",") + exchange.setProperty("stockSymbols", result) + log.info("📊 Extracted symbols: {}", result) + - log: "Symbols extracted: ${exchangeProperty.stockSymbols}" + + # Step 4: Fetch market data for extracted symbols from MarketMind API + - choice: + when: + - simple: "${exchangeProperty.stockSymbols} != 'NONE' && ${exchangeProperty.stockSymbols} != null" + steps: + - script: + groovy: | + import java.net.URL + import java.net.HttpURLConnection + + def symbols = exchange.getProperty("stockSymbols", String.class) + def symbolList = symbols.split(",").collect { it.trim().toUpperCase() }.findAll { it } + def marketUrl = camelContext.resolvePropertyPlaceholders("{{marketmind.api.url}}") + + log.info("Fetching market data for {} symbols from {}", symbolList.size(), marketUrl) + + def jsonParts = [] + + symbolList.each { symbol -> + try { + log.info("Fetching data for symbol: {}", symbol) + + // Fetch quote data + def quoteUrl = new URL("http://${marketUrl}/quote/${symbol}") + def quoteConn = quoteUrl.openConnection() as HttpURLConnection + quoteConn.setRequestMethod("GET") + quoteConn.setConnectTimeout(10000) + quoteConn.setReadTimeout(10000) + + def quoteData = "{}" + if (quoteConn.responseCode == 200) { + quoteData = quoteConn.inputStream.text + } else { + log.warn("Failed to fetch quote for {}: {}", symbol, quoteConn.responseCode) + } + quoteConn.disconnect() + + // Fetch analyst data + def analystUrl = new URL("http://${marketUrl}/analyst/${symbol}") + def analystConn = analystUrl.openConnection() as HttpURLConnection + analystConn.setRequestMethod("GET") + analystConn.setConnectTimeout(10000) + analystConn.setReadTimeout(10000) + + def analystData = "{}" + if (analystConn.responseCode == 200) { + analystData = analystConn.inputStream.text + } else { + log.warn("Failed to fetch analyst for {}: {}", symbol, analystConn.responseCode) + } + analystConn.disconnect() + + // Build JSON part for this symbol + def part = ' "' + symbol + '": {\n' + part += ' "quote": ' + quoteData + ',\n' + part += ' "analyst": ' + analystData + '\n' + part += ' }' + + jsonParts << part + log.info("Successfully fetched data for {}", symbol) + + } catch (Exception e) { + log.error("Error fetching data for {}: {}", symbol, e.message) + // Add empty data for failed symbols + def part = ' "' + symbol + '": {\n' + part += ' "quote": {},\n' + part += ' "analyst": {}\n' + part += ' }' + jsonParts << part + } + } + + def jsonStr = "{\n" + jsonParts.join(",\n") + "\n}" + exchange.setProperty("marketIntelligence", jsonStr) + log.info("Market intelligence data collected for {} symbols", symbolList.size()) + otherwise: + steps: + - setProperty: + name: marketIntelligence + constant: "No stock symbols found" + + # Step 5: Perform comprehensive AI analysis with market context + - log: "🤖 Performing AI analysis with market intelligence (this may take several minutes for large documents)..." + - setBody: + simple: | + You are a financial analyst AI assistant. Analyze the following financial document and provide: + + 1. **Executive Summary** (3-4 sentences) + 2. **Key Financial Metrics** (revenue, profit, growth rates, etc.) + 3. **Companies and Stock Symbols Mentioned** + 4. **Investment Thesis** (bullish/bearish signals based on fundamentals) + 5. **Risk Factors** identified + 6. **Market Context** (based on provided market data below) + 7. **Actionable Insights** for investors + + Stock Symbols Identified: ${exchangeProperty.stockSymbols} + + Document Content: + ${exchangeProperty.documentContent} + + Market Intelligence Data (including analyst consensus ratings): + ${exchangeProperty.marketIntelligence} + + Provide a comprehensive, professional analysis suitable for investment decision-making. + - to: + uri: langchain4j-chat:financial-analysis + parameters: + chatModel: "#chatModel" + - setProperty: + name: aiAnalysis + simple: "${body}" + - log: "✅ AI analysis completed" + + # Step 5b: Extract analyst ratings from MarketMind data for report + - script: + groovy: | + import java.util.regex.Pattern + + def marketIntel = exchange.getProperty("marketIntelligence", String.class) + def symbols = exchange.getProperty("stockSymbols", String.class) + + def analystRatingsText = "" + + try { + def analystSummaries = [] + + // Simple string search approach + symbols.split(",").each { sym -> + def symbol = sym.trim().toUpperCase() + + // Extract values using simple patterns + def consensus = null + def targetPrice = null + def currentPrice = null + def change = null + def changePct = null + + // Look for recommendation + def consensusMatch = (marketIntel =~ /(?s)"${symbol}".*?"recommendation"\s*:\s*"([^"]+)"/) + if (consensusMatch.find()) { + consensus = consensusMatch.group(1) + } + + // Look for target_mean + def targetMatch = (marketIntel =~ /(?s)"${symbol}".*?"target_mean"\s*:\s*([0-9.]+)/) + if (targetMatch.find()) { + targetPrice = Double.parseDouble(targetMatch.group(1)) + } + + // Look for current_price + def priceMatch = (marketIntel =~ /(?s)"${symbol}".*?"current_price"\s*:\s*([0-9.]+)/) + if (priceMatch.find()) { + currentPrice = Double.parseDouble(priceMatch.group(1)) + } + + // Look for change + def changeMatch = (marketIntel =~ /(?s)"${symbol}".*?"change"\s*:\s*([+-]?[0-9.]+)/) + if (changeMatch.find()) { + change = Double.parseDouble(changeMatch.group(1)) + } + + // Look for change_percent + def changePctMatch = (marketIntel =~ /(?s)"${symbol}".*?"change_percent"\s*:\s*([+-]?[0-9.]+)/) + if (changePctMatch.find()) { + changePct = Double.parseDouble(changePctMatch.group(1)) + } + + // Build narrative if we have minimum data + if (consensus && currentPrice != null) { + def narrative = "${symbol} is currently trading at \$${String.format('%.2f', currentPrice)}" + + if (change != null && changePct != null) { + narrative += " (${String.format('%+.2f', change)}, ${String.format('%+.2f%%', changePct)})" + } + + narrative += ". Analysts have a consensus rating of '${consensus}'" + + if (targetPrice != null) { + narrative += " with a target price of \$${String.format('%.2f', targetPrice)}" + def upside = ((targetPrice - currentPrice) / currentPrice) * 100 + narrative += ", indicating ${String.format('%+.1f%%', upside)} potential from current levels" + } + + narrative += "." + + analystSummaries << [symbol: symbol, consensus: consensus, narrative: narrative] + } + } + + // Build analyst ratings section for report + if (!analystSummaries.isEmpty()) { + analystRatingsText = "## Analyst Ratings Summary\n\n" + analystSummaries.each { summary -> + analystRatingsText += "**${summary.symbol}** (${summary.consensus.toUpperCase()})\n\n" + analystRatingsText += "${summary.narrative}\n\n" + } + } + + // Store for logging later + exchange.setProperty("analystSummaries", analystSummaries) + exchange.setProperty("analystRatingsText", analystRatingsText) + + } catch (Exception e) { + log.error("Error extracting analyst ratings: ${e.message}") + } + + # Step 6: Generate comprehensive investment report + - script: + groovy: | + import java.text.SimpleDateFormat + + def fileName = exchange.getProperty("originalFileName") + def documentContent = exchange.getProperty("documentContent", String.class) + def symbols = exchange.getProperty("stockSymbols", String.class) + def marketIntel = exchange.getProperty("marketIntelligence", String.class) + def analysis = exchange.getProperty("aiAnalysis", String.class) + def analystRatings = exchange.getProperty("analystRatingsText", String.class) + def dateStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + + // Build report using string concatenation to avoid backtick issues + def report = '# Financial Document Analysis Report\n\n' + report += 'Source Document: ' + fileName + '\n\n' + report += 'Analysis Date: ' + dateStr + '\n\n' + report += 'Stock Symbols Identified: ' + (symbols ?: 'None') + '\n\n\n\n' + + // Add Analyst Ratings Summary (if available) + if (analystRatings) { + report += '---\n\n' + report += analystRatings + '\n' + report += '---\n\n' + } + + report += '## AI-Powered Financial Analysis\n\n' + report += analysis + '\n\n\n\n' + + exchange.message.setBody(report) + + # Step 7: Save the analysis report + - setHeader: + name: CamelFileName + simple: "${exchangeProperty.originalFileName}_analysis_${date:now:yyyyMMdd_HHmmss}.md" + - to: + uri: file:{{output.directory}} + - log: "💾 Analysis report saved: ${header.CamelFileName}" + + # Step 8: Log analyst ratings to console + - script: + groovy: | + def analystSummaries = exchange.getProperty("analystSummaries", List.class) + + if (analystSummaries == null || analystSummaries.isEmpty()) { + log.warn("⚠️ No analyst ratings found in MarketMind data") + } else { + log.info("=" * 80) + log.info("📊 ANALYST RATINGS SUMMARY") + log.info("=" * 80) + log.info("") + + analystSummaries.each { summary -> + // Emoji based on consensus + def emoji = "" + def rating = summary.consensus.toUpperCase() + switch(rating) { + case ~/.*STRONG.?BUY.*/: + emoji = "🚀" + break + case ~/.*BUY.*/: + emoji = "✅" + break + case ~/.*OUTPERFORM.*/: + emoji = "📈" + break + case ~/.*HOLD.*/: + emoji = "⏸️" + break + case ~/.*UNDERPERFORM.*/: + emoji = "📉" + break + case ~/.*SELL.*/: + emoji = "❌" + break + default: + emoji = "ℹ️" + } + + log.info("${emoji} ${summary.symbol}") + log.info(" ${summary.narrative}") + log.info("") + } + log.info("=" * 80) + } + + - log: "✨ Processing complete for: ${exchangeProperty.originalFileName} (source file will be deleted automatically)" + +# Route 2: Market Data API Endpoint (mock) +- route: + id: get-market-data + from: + uri: platform-http:/api/market-data/{symbol} + steps: + - log: "Fetching market data for: ${header.symbol}" + - toD: + uri: "http:{{marketmind.api.url}}/quote/${header.symbol}" + parameters: + httpMethod: GET + - setHeader: + name: Content-Type + constant: "application/json" + +# Route 3: Interactive Document Analysis API +- route: + id: analyze-document-api + from: + uri: platform-http:/api/analyze + parameters: + httpMethodRestrict: "POST" + steps: + - log: "📥 Received document for analysis via API" + - setProperty: + name: uploadedDocument + simple: "${body}" + + # Convert document using Docling + - log: "Converting uploaded document..." + - to: + uri: docling:CONVERT_TO_MARKDOWN + parameters: + useDoclingServe: true + doclingServeUrl: "{{docling.serve.url}}" + contentInBody: true + - setProperty: + name: convertedContent + simple: "${body}" + + # Extract symbols + - setBody: + simple: | + Extract stock ticker symbols from this document. + Return only a comma-separated list of symbols or "NONE". + + ${exchangeProperty.convertedContent} + - to: + uri: langchain4j-chat:extract + parameters: + chatModel: "#chatModel" + - setProperty: + name: symbols + simple: "${body}" + + # Generate analysis + - setBody: + simple: | + Provide a brief investment-focused analysis of this document. + Symbols found: ${exchangeProperty.symbols} + + Document: + ${exchangeProperty.convertedContent} + - to: + uri: langchain4j-chat:quick-analysis + parameters: + chatModel: "#chatModel" + - setHeader: + name: Content-Type + constant: "application/json" + - script: + groovy: | + def symbols = exchange.getProperty("symbols") + def analysis = exchange.message.getBody(String.class).replaceAll('"', '\\\\"') + def timestamp = new Date().toString() + + def json = '{\n' + json += ' "symbols": "' + symbols + '",\n' + json += ' "analysis": "' + analysis + '",\n' + json += ' "timestamp": "' + timestamp + '"\n' + json += '}' + + exchange.message.setBody(json) + +# Route 5: Health Check +- route: + id: health-check + from: + uri: platform-http:/api/health + steps: + - log: "Health check requested" + - script: + groovy: | + def doclingUrl = camelContext.resolvePropertyPlaceholders("{{docling.serve.url}}") + def ollamaUrl = camelContext.resolvePropertyPlaceholders("{{ollama.base.url}}") + def ollamaModel = camelContext.resolvePropertyPlaceholders("{{ollama.model.name}}") + def docsDir = camelContext.resolvePropertyPlaceholders("{{documents.directory}}") + def outDir = camelContext.resolvePropertyPlaceholders("{{output.directory}}") + def marketUrl = camelContext.resolvePropertyPlaceholders("{{marketmind.api.url}}") + + def json = '{\n' + json += ' "status": "healthy",\n' + json += ' "timestamp": "' + new Date().toString() + '",\n' + json += ' "components": {\n' + json += ' "docling": {\n' + json += ' "url": "' + doclingUrl + '",\n' + json += ' "status": "configured"\n' + json += ' },\n' + json += ' "ollama": {\n' + json += ' "url": "' + ollamaUrl + '",\n' + json += ' "model": "' + ollamaModel + '",\n' + json += ' "status": "configured"\n' + json += ' },\n' + json += ' "marketmind_api": {\n' + json += ' "url": "' + marketUrl + '",\n' + json += ' "type": "MarketMind REST API",\n' + json += ' "status": "configured"\n' + json += ' }\n' + json += ' },\n' + json += ' "directories": {\n' + json += ' "documents": "' + docsDir + '",\n' + json += ' "output": "' + outDir + '"\n' + json += ' }\n' + json += '}' + + exchange.message.setBody(json) + - setHeader: + name: Content-Type + constant: "application/json"
