This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git
The following commit(s) were added to refs/heads/main by this push:
new 4ef549a Financial Analyzer example (#45)
4ef549a is described below
commit 4ef549a7318e3861d9e966f29118573c260a38a7
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Oct 20 13:42:38 2025 +0200
Financial Analyzer example (#45)
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"