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"


Reply via email to