You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2025/10/20 11:40:47 UTC
(camel-jbang-examples) 03/03: Financial Analyzer example
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 aae9c3405ea9d5dab6edd342cbc985091bb8e147
Author: Andrea Cosentino <an...@gmail.com>
AuthorDate: Mon Oct 20 13:40:05 2025 +0200
Financial Analyzer example
Signed-off-by: Andrea Cosentino <an...@gmail.com>
---
financial-doc-analyzer/README.adoc | 288 +++++++++++
financial-doc-analyzer/application.properties | 44 ++
.../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 +++++++++++++++++++++
7 files changed, 872 insertions(+)
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/financial-doc-analyzer/application.properties b/financial-doc-analyzer/application.properties
new file mode 100644
index 0000000..2f6ee6a
--- /dev/null
+++ b/financial-doc-analyzer/application.properties
@@ -0,0 +1,44 @@
+# 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.
+
+# 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"