diff --git a/financial/README.md b/financial/README.md
new file mode 100644
index 00000000..72d52f59
--- /dev/null
+++ b/financial/README.md
@@ -0,0 +1,91 @@
+Write up a demo script using the format and style in the attached docx file but for the following workshop....
+"
+This financial application and its corresponding workshop are aimed at both financial line of business experts and developers.
+It is intended to allow the two personas to have a common ground and shared understanding of the possibilities and finer details of both the business solutions and the development architecture, features, etc. involved in them.
+
+Each lab/part of the application describes various aspects including...
+- business process
+- dev/tech involved
+- existing companies that use such solutions
+- differentiators
+- low level details of code, tests, and comparisons with other solutions/vendors
+- brief video walkthrough
+
+The workshop in addition goes into further details on...
+- migration
+- scaling and sizing
+- expert contacts in product management, etc.
+
+Introduction
+The application used is a full-stack, microservices based architecture, using all of the latest and most popular developer technologies.
+The frontend is written predominantly in React.
+The mid-tier is written in various languages and platforms such as Java and Spring Boot, React and Node, Python, .NET, Go, Rust, and WASM
+Oracle Database serves as the backend, however, Oracle database is far from just a storage mechanism as you will see as you go through the labs.
+A number or Oracle and Oracle database technologies are used.
+
+Lab 1
+Process: FinTech/Bank APIs: Access/use financial data or processes from APIs. Display the largest changes in portfolio
+Tech Used: Oracle Rest Data Services (ORDS)
+Reference: Bank Of India
+Differentiators: Create APIs from data an processes in under a minute
+Low-level details: Comparison of speed with other API creation methods as well as the advantage of ORDS
+
+Lab 2
+Process: DevOps: Kubernetes, Microservices, and Observability
+Tech Used: Oracle Backend For Microservices and AI, OpenTelemetry, Grafan
+Reference: LOLC
+Differentiators: Simplified management of Kubernetes and microservices, one of a kind trace exporter in the database, giving the ability to trace *into* the database that no other vendor has, as well as metrics and log exports - all exporters accept SQL for the most advanced querying of data.
+Low-level details: Realize the amount of architecture that is automated and the convenience, and time saving time-to-mark advantages
+
+Lab 3
+Process: Create and Query Accounts: Create with MongoDB API, query with SQL
+Tech Used: MongoDB API adapter, JSON Duality
+Reference: Santander
+Differentiators: Use JSON Duality for seamless SQL querying of the same data. No other database can do this.
+Low-level details: Instigate crash and notice transactionality of Oracle Database for JSON and relational.
+
+Lab 4
+Process: Transfer funds between banks
+Tech Used: Spring Boot, MicroTx, Lock-free reservations
+Reference: U of Naples
+Differentiators: The only database that provides auto-compensating sagas (microservice transactions) and highest throughput for hotspots. Simplified development (~80% less code)
+Low-level details: Instigate crash and notice automatic recovery that is possible and the huge amount of error-prone code that would be required otherwise.
+
+Lab 5
+Process: Credit card purchases, fraud, and money laundering
+Tech Used: Credit card purchases are conducted using Oracle Globally Distributed Database.
+Fraud detection and visualization is conducted using OML4Py (Python) and Spatial.
+Money Laundering is detected using Oracle Graph.
+Events are sent using Knative Eventing and CloudEvents.
+Reference: AMEX
+Differentiators:
+Low-level details:
+
+Lab 6
+Process: Transfer to brokerage accounts
+Tech Used: Kafka and TxEventQ
+Reference: FSGUB
+Differentiators:
+Low-level details: Instigate crash and notice message duplication, message loss, data duplication, and additional code required when using Kafka with Postgres and MongoDB that is automatically and transactionally handled when using Kafka with Oracle Database.
+
+Lab 7
+Process: Stock ticker and buy/sell stock
+Tech Used: TrueCache, Polyglot (Java, JS, Python, .NET, Go, Rust, PL/SQL)
+Reference: NYSE
+Differentiators: Unlike Redis, True Cache uses SQL, not a proprietary API
+Low-level details:
+
+Lab 8
+Process: Personalized Financial Insights
+Tech Used: Vector Search, AI Agents, and MCP
+Reference: Merrill Lynch
+Differentiators: Access data securely from Oracle Database hub. Even using JavaScript and Java from within the database to make MCP AI Agent calls
+Low-level details:
+
+Lab 9 Speak with your financial data
+Process: Access/use financial data or processes from APIs. Display the largest changes in portfolio
+Tech Used: NL2SQL/Select AI, Vector Search, Oracle AI Explorer, Speech AI
+Reference: various call centers
+Differentiators:
+Low-level details:
+""
diff --git a/financial/ai-agents/.gitignore b/financial/ai-agents/.gitignore
new file mode 100644
index 00000000..a7b9b8c3
--- /dev/null
+++ b/financial/ai-agents/.gitignore
@@ -0,0 +1,29 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Virtual Environment
+venv/
+env/
+.env
+
+# IDE
+.vscode/
+.idea/
+
+# Gradio
+.gradio/
+
+# Generated files
+embeddings/
+chroma_db/
+docs/*.json
+
+# Distribution / packaging
+dist/
+build/
+*.egg-info/
+
+# Logs
+*.log
\ No newline at end of file
diff --git a/financial/ai-agents/OraDBVectorStore.py b/financial/ai-agents/OraDBVectorStore.py
new file mode 100644
index 00000000..14b8c3a3
--- /dev/null
+++ b/financial/ai-agents/OraDBVectorStore.py
@@ -0,0 +1,459 @@
+from typing import List, Dict, Any
+import json
+import argparse
+from sentence_transformers import SentenceTransformer
+import array
+import oracledb
+import yaml
+import os
+from pathlib import Path
+
+
+class OraDBVectorStore:
+ def __init__(self, persist_directory: str = "embeddings"):
+ """Initialize Oracle DB Vector Store
+
+ Args:
+ persist_directory: Not used for Oracle DB connection but kept for compatibility
+ """
+ # Load Oracle DB credentials from config.yaml
+ credentials = self._load_config()
+
+ username = credentials.get("ORACLE_DB_USERNAME", "ADMIN")
+ password = credentials.get("ORACLE_DB_PASSWORD", "")
+ dsn = credentials.get("ORACLE_DB_DSN", "")
+
+ if not password or not dsn:
+ raise ValueError("Oracle DB credentials not found in config.yaml. Please set ORACLE_DB_USERNAME, ORACLE_DB_PASSWORD, and ORACLE_DB_DSN.")
+
+ # Connect to the database
+ try:
+ conn23c = oracledb.connect(user=username, password=password, dsn=dsn)
+ print("Oracle DB Connection successful!")
+ except Exception as e:
+ print("Oracle DB Connection failed!", e)
+ raise
+
+ # Create a table to store the data
+ cursor = conn23c.cursor()
+
+ self.connection = conn23c
+ self.cursor = cursor
+
+ sql = """CREATE TABLE IF NOT EXISTS PDFCollection (
+ id VARCHAR2(4000 BYTE) PRIMARY KEY,
+ text VARCHAR2(4000 BYTE),
+ metadata VARCHAR2(4000 BYTE),
+ embedding VECTOR
+ )"""
+
+ cursor.execute(sql)
+
+ sql = """CREATE TABLE IF NOT EXISTS WebCollection (
+ id VARCHAR2(4000 BYTE) PRIMARY KEY,
+ text VARCHAR2(4000 BYTE),
+ metadata VARCHAR2(4000 BYTE),
+ embedding VECTOR
+ )"""
+
+ cursor.execute(sql)
+
+ sql = """CREATE TABLE IF NOT EXISTS RepoCollection (
+ id VARCHAR2(4000 BYTE) PRIMARY KEY,
+ text VARCHAR2(4000 BYTE),
+ metadata VARCHAR2(4000 BYTE),
+ embedding VECTOR
+ )"""
+
+ cursor.execute(sql)
+
+
+ sql = """CREATE TABLE IF NOT EXISTS GeneralCollection (
+ id VARCHAR2(4000 BYTE) PRIMARY KEY,
+ text VARCHAR2(4000 BYTE),
+ metadata VARCHAR2(4000 BYTE),
+ embedding VECTOR
+ )"""
+
+ cursor.execute(sql)
+
+ self.encoder = SentenceTransformer('all-MiniLM-L12-v2')
+
+
+ def _load_config(self) -> Dict[str, str]:
+ """Load configuration from config.yaml"""
+ try:
+ config_path = Path("config.yaml")
+ if not config_path.exists():
+ print("Warning: config.yaml not found. Using empty configuration.")
+ return {}
+
+ with open(config_path, 'r') as f:
+ config = yaml.safe_load(f)
+ return config if config else {}
+ except Exception as e:
+ print(f"Warning: Error loading config: {str(e)}")
+ return {}
+
+ def _sanitize_metadata(self, metadata: Dict) -> Dict:
+ """Sanitize metadata to ensure all values are valid types for Oracle DB"""
+ sanitized = {}
+ for key, value in metadata.items():
+ if isinstance(value, (str, int, float, bool)):
+ sanitized[key] = value
+ elif isinstance(value, list):
+ # Convert list to string representation
+ sanitized[key] = str(value)
+ elif value is None:
+ # Replace None with empty string
+ sanitized[key] = ""
+ else:
+ # Convert any other type to string
+ sanitized[key] = str(value)
+ return sanitized
+
+ def add_pdf_chunks(self, chunks: List[Dict[str, Any]], document_id: str):
+ """Add chunks from a PDF document to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for Oracle DB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{document_id}_{i}" for i in range(len(chunks))]
+
+ # Encode all texts in a batch
+ embeddings = self.encoder.encode(texts, batch_size=32, show_progress_bar=True)
+
+ table_name = "PDFCollection"
+ # Truncate the table
+ self.cursor.execute(f"truncate table {table_name}")
+
+ # Insert embeddings into Oracle
+ for i, (docid, text, metadata, embedding) in enumerate(zip(ids, texts, metadatas, embeddings), start=1):
+ json_metadata = json.dumps(metadata) # Convert to JSON string
+ vector = array.array("f", embedding)
+
+ self.cursor.execute(
+ "INSERT INTO PDFCollection (id, text, metadata, embedding) VALUES (:1, :2, :3, :4)",
+ (docid, text, json_metadata, vector)
+ )
+
+ self.connection.commit()
+
+ def add_web_chunks(self, chunks: List[Dict[str, Any]], source_id: str):
+ """Add chunks from web content to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for Oracle DB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{source_id}_{i}" for i in range(len(chunks))]
+
+ # Encode all texts in a batch
+ embeddings = self.encoder.encode(texts, batch_size=32, show_progress_bar=True)
+
+ table_name = "WebCollection"
+ # No truncation for web chunks, just append new ones
+
+ # Insert embeddings into Oracle
+ for i, (docid, text, metadata, embedding) in enumerate(zip(ids, texts, metadatas, embeddings), start=1):
+ json_metadata = json.dumps(metadata) # Convert to JSON string
+ vector = array.array("f", embedding)
+
+ self.cursor.execute(
+ "INSERT INTO WebCollection (id, text, metadata, embedding) VALUES (:1, :2, :3, :4)",
+ (docid, text, json_metadata, vector)
+ )
+
+ self.connection.commit()
+
+ def add_general_knowledge(self, chunks: List[Dict[str, Any]], source_id: str):
+ """Add general knowledge chunks to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for Oracle DB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{source_id}_{i}" for i in range(len(chunks))]
+
+ # Encode all texts in a batch
+ embeddings = self.encoder.encode(texts, batch_size=32, show_progress_bar=True)
+
+ table_name = "GeneralCollection"
+
+ # Insert embeddings into Oracle
+ for i, (docid, text, metadata, embedding) in enumerate(zip(ids, texts, metadatas, embeddings), start=1):
+ json_metadata = json.dumps(metadata) # Convert to JSON string
+ vector = array.array("f", embedding)
+
+ self.cursor.execute(
+ "INSERT INTO GeneralCollection (id, text, metadata, embedding) VALUES (:1, :2, :3, :4)",
+ (docid, text, json_metadata, vector)
+ )
+
+ self.connection.commit()
+
+ def add_repo_chunks(self, chunks: List[Dict[str, Any]], document_id: str):
+ """Add chunks from a repository to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for Oracle DB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{document_id}_{i}" for i in range(len(chunks))]
+
+ # Encode all texts in a batch
+ embeddings = self.encoder.encode(texts, batch_size=32, show_progress_bar=True)
+
+ table_name = "RepoCollection"
+
+ # Insert embeddings into Oracle
+ for i, (docid, text, metadata, embedding) in enumerate(zip(ids, texts, metadatas, embeddings), start=1):
+ json_metadata = json.dumps(metadata) # Convert to JSON string
+ vector = array.array("f", embedding)
+
+ self.cursor.execute(
+ "INSERT INTO RepoCollection (id, text, metadata, embedding) VALUES (:1, :2, :3, :4)",
+ (docid, text, json_metadata, vector)
+ )
+
+ self.connection.commit()
+
+ def query_pdf_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the PDF documents collection"""
+ print("🔍 [Oracle DB] Querying PDF Collection")
+ # Generate Embeddings
+ embeddings = self.encoder.encode(query, batch_size=32, show_progress_bar=True)
+ new_vector = array.array("f", embeddings)
+
+ sql = """
+ SELECT Id, Text, MetaData, Embedding
+ FROM PDFCOLLECTION
+ ORDER BY VECTOR_DISTANCE(EMBEDDING, :nv, EUCLIDEAN)
+ FETCH FIRST 10 ROWS ONLY
+ """
+
+ self.cursor.execute(sql, {"nv": new_vector})
+
+ # Fetch all rows
+ rows = self.cursor.fetchall()
+
+ # Format results
+ formatted_results = []
+ for row in rows:
+ result = {
+ "content": row[1],
+ "metadata": json.loads(row[2]) if isinstance(row[2], str) else row[2]
+ }
+ formatted_results.append(result)
+
+ print(f"🔍 [Oracle DB] Retrieved {len(formatted_results)} chunks from PDF Collection")
+ return formatted_results
+
+ def query_web_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the web documents collection"""
+ print("🔍 [Oracle DB] Querying Web Collection")
+ # Generate Embeddings
+ embeddings = self.encoder.encode(query, batch_size=32, show_progress_bar=True)
+ new_vector = array.array("f", embeddings)
+
+ sql = """
+ SELECT Id, Text, MetaData, Embedding
+ FROM WebCOLLECTION
+ ORDER BY VECTOR_DISTANCE(EMBEDDING, :nv, EUCLIDEAN)
+ FETCH FIRST 10 ROWS ONLY
+ """
+
+ self.cursor.execute(sql, {"nv": new_vector})
+
+ # Fetch all rows
+ rows = self.cursor.fetchall()
+
+ # Format results
+ formatted_results = []
+ for row in rows:
+ result = {
+ "content": row[1],
+ "metadata": json.loads(row[2]) if isinstance(row[2], str) else row[2]
+ }
+ formatted_results.append(result)
+
+ print(f"🔍 [Oracle DB] Retrieved {len(formatted_results)} chunks from Web Collection")
+ return formatted_results
+
+ def query_general_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the general knowledge collection"""
+ print("🔍 [Oracle DB] Querying General Knowledge Collection")
+ # Generate Embeddings
+ embeddings = self.encoder.encode(query, batch_size=32, show_progress_bar=True)
+ new_vector = array.array("f", embeddings)
+
+ sql = """
+ SELECT Id, Text, MetaData, Embedding
+ FROM GeneralCollection
+ ORDER BY VECTOR_DISTANCE(EMBEDDING, :nv, EUCLIDEAN)
+ FETCH FIRST 10 ROWS ONLY
+ """
+
+ self.cursor.execute(sql, {"nv": new_vector})
+
+ # Fetch all rows
+ rows = self.cursor.fetchall()
+
+ # Format results
+ formatted_results = []
+ for row in rows:
+ result = {
+ "content": row[1],
+ "metadata": json.loads(row[2]) if isinstance(row[2], str) else row[2]
+ }
+ formatted_results.append(result)
+
+ print(f"🔍 [Oracle DB] Retrieved {len(formatted_results)} chunks from General Knowledge Collection")
+ return formatted_results
+
+ def query_repo_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the repository documents collection"""
+ print("🔍 [Oracle DB] Querying Repository Collection")
+ # Generate Embeddings
+ embeddings = self.encoder.encode(query, batch_size=32, show_progress_bar=True)
+ new_vector = array.array("f", embeddings)
+
+ sql = """
+ SELECT Id, Text, MetaData, Embedding
+ FROM RepoCOLLECTION
+ ORDER BY VECTOR_DISTANCE(EMBEDDING, :nv, EUCLIDEAN)
+ FETCH FIRST 10 ROWS ONLY
+ """
+
+ self.cursor.execute(sql, {"nv": new_vector})
+
+ # Fetch all rows
+ rows = self.cursor.fetchall()
+
+ # Format results
+ formatted_results = []
+ for row in rows:
+ result = {
+ "content": row[1],
+ "metadata": json.loads(row[2]) if isinstance(row[2], str) else row[2]
+ }
+ formatted_results.append(result)
+
+ print(f"🔍 [Oracle DB] Retrieved {len(formatted_results)} chunks from Repository Collection")
+ return formatted_results
+
+ def get_collection_count(self, collection_name: str) -> int:
+ """Get the total number of chunks in a collection
+
+ Args:
+ collection_name: Name of the collection (pdf_documents, web_documents, repository_documents, general_knowledge)
+
+ Returns:
+ Number of chunks in the collection
+ """
+ # Map collection names to table names
+ collection_map = {
+ "pdf_documents": "PDFCollection",
+ "web_documents": "WebCollection",
+ "repository_documents": "RepoCollection",
+ "general_knowledge": "GeneralCollection"
+ }
+
+ table_name = collection_map.get(collection_name)
+ if not table_name:
+ raise ValueError(f"Unknown collection name: {collection_name}")
+
+ # Count the rows in the table
+ sql = f"SELECT COUNT(*) FROM {table_name}"
+ self.cursor.execute(sql)
+ count = self.cursor.fetchone()[0]
+
+ return count
+
+ def get_latest_chunk(self, collection_name: str) -> Dict[str, Any]:
+ """Get the most recently inserted chunk from a collection
+
+ Args:
+ collection_name: Name of the collection (pdf_documents, web_documents, repository_documents, general_knowledge)
+
+ Returns:
+ Dictionary containing the content and metadata of the latest chunk
+ """
+ # Map collection names to table names
+ collection_map = {
+ "pdf_documents": "PDFCollection",
+ "web_documents": "WebCollection",
+ "repository_documents": "RepoCollection",
+ "general_knowledge": "GeneralCollection"
+ }
+
+ table_name = collection_map.get(collection_name)
+ if not table_name:
+ raise ValueError(f"Unknown collection name: {collection_name}")
+
+ # Get the most recently inserted row (using ID as a proxy for insertion time)
+ # This assumes IDs are assigned sequentially or have a timestamp component
+ sql = f"SELECT Id, Text, MetaData FROM {table_name} ORDER BY ROWID DESC FETCH FIRST 1 ROW ONLY"
+ self.cursor.execute(sql)
+ row = self.cursor.fetchone()
+
+ if not row:
+ raise ValueError(f"No chunks found in collection: {collection_name}")
+
+ result = {
+ "id": row[0],
+ "content": row[1],
+ "metadata": json.loads(row[2]) if isinstance(row[2], str) else row[2]
+ }
+
+ return result
+
+def main():
+ parser = argparse.ArgumentParser(description="Manage Oracle DB vector store")
+ parser.add_argument("--add", help="JSON file containing chunks to add")
+ parser.add_argument("--add-web", help="JSON file containing web chunks to add")
+ parser.add_argument("--query", help="Query to search for")
+
+ args = parser.parse_args()
+ store = OraDBVectorStore()
+
+ if args.add:
+ with open(args.add, 'r', encoding='utf-8') as f:
+ chunks = json.load(f)
+ store.add_pdf_chunks(chunks, document_id=args.add)
+ print(f"✓ Added {len(chunks)} PDF chunks to Oracle DB vector store")
+
+ if args.add_web:
+ with open(args.add_web, 'r', encoding='utf-8') as f:
+ chunks = json.load(f)
+ store.add_web_chunks(chunks, source_id=args.add_web)
+ print(f"✓ Added {len(chunks)} web chunks to Oracle DB vector store")
+
+ if args.query:
+ # Query both collections
+ pdf_results = store.query_pdf_collection(args.query)
+ web_results = store.query_web_collection(args.query)
+
+ print("\nPDF Results:")
+ print("-" * 50)
+ for result in pdf_results:
+ print(f"Content: {result['content'][:200]}...")
+ print(f"Source: {result['metadata'].get('source', 'Unknown')}")
+ print(f"Pages: {result['metadata'].get('page_numbers', [])}")
+ print("-" * 50)
+
+ print("\nWeb Results:")
+ print("-" * 50)
+ for result in web_results:
+ print(f"Content: {result['content'][:200]}...")
+ print(f"Source: {result['metadata'].get('source', 'Unknown')}")
+ print(f"Title: {result['metadata'].get('title', 'Unknown')}")
+ print("-" * 50)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/README.md b/financial/ai-agents/README.md
new file mode 100644
index 00000000..b7ed6f49
--- /dev/null
+++ b/financial/ai-agents/README.md
@@ -0,0 +1,469 @@
+# Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph)
+
+## Introduction
+
+An intelligent RAG (Retrieval Augmented Generation) system that uses an LLM agent to make decisions about information retrieval and response generation. The system processes PDF documents and can intelligently decide which knowledge base to query based on the user's question.
+
+
+
+The system has the following features:
+
+- Intelligent query routing
+- PDF processing using Docling for accurate text extraction and chunking
+- Persistent vector storage with Oracle Database 23ai (PDF and Websites)
+- Smart context retrieval and response generation
+- FastAPI-based REST API for document upload and querying
+- Support for both OpenAI-based agents or local, transformer-based agents (`Mistral-7B` by default)
+- Support for quantized models (4-bit/8-bit) and Ollama models for faster inference
+- Optional Chain of Thought (CoT) reasoning for more detailed and structured responses
+
+
+
+
+
+
+
+Here you can find a result of using Chain of Thought (CoT) reasoning:
+
+
+
+## 0. Prerequisites and setup
+
+### Prerequisites
+
+- Python 3.8 or higher
+- OpenAI API key (optional, for OpenAI-based agent)
+- HuggingFace token (optional, for local Mistral model)
+
+### Hardware Requirements
+
+- For the OpenAI Agent: Standard CPU machine
+- For the Local Agent:
+ - Minimum 16GB RAM (recommended >24GBs)
+ - GPU with 8GB VRAM recommended for better performance
+ - Will run on CPU if GPU is not available, but will be significantly slower.
+ - For quantized models (4-bit/8-bit): Reduced VRAM requirements (4-6GB) with minimal performance impact
+ - For Ollama models: Requires Ollama to be installed and running, with significantly reduced memory requirements
+
+### Setup
+
+1. Clone the repository and install dependencies:
+
+ ```bash
+ git clone https://github.com/oracle-devrel/devrel-labs.git
+ cd devrel-labs/agentic_rag
+ pip install -r requirements.txt
+ ```
+
+2. Authenticate with HuggingFace (for Hugging Face models only):
+
+ The system uses `Mistral-7B` by default, which requires authentication with HuggingFace:
+
+ a. Create a HuggingFace account [here](https://huggingface.co/join), if you don't have one yet.
+
+ b. Accept the Mistral-7B model terms & conditions [here](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2)
+
+ c. Create an access token [here](https://huggingface.co/settings/tokens)
+
+ d. Create a `config.yaml` file (you can copy from `config_example.yaml`), and add your HuggingFace token:
+ ```yaml
+ HUGGING_FACE_HUB_TOKEN: your_token_here
+ ```
+
+3. (Optional) If you want to use the OpenAI-based agent instead of the default local model, create a `.env` file with your OpenAI API key:
+
+ ```bash
+ OPENAI_API_KEY=your-api-key-here
+ ```
+
+ If no API key is provided, the system will automatically download and use `Mistral-7B-Instruct-v0.2` for text generation when using the local model. No additional configuration is needed.
+
+4. For quantized models, ensure bitsandbytes is installed:
+
+ ```bash
+ pip install bitsandbytes>=0.41.0
+ ```
+
+5. For Ollama models, install Ollama:
+
+ a. Download and install Ollama from [ollama.com/download](https://ollama.com/download) for Windows, or run the following command in Linux:
+
+ ```bash
+ curl -fsSL https://ollama.com/install.sh | sh
+ ```
+
+ b. Start the Ollama service
+
+ c. Pull the models you want to use beforehand:
+
+ ```bash
+ ollama pull llama3
+ ollama pull phi3
+ ollama pull qwen2
+ ```
+
+## 1. Getting Started
+
+You can launch this solution in three ways:
+
+### 1. Using the Complete REST API
+
+Start the API server:
+
+```bash
+python main.py
+```
+
+The API will be available at `http://localhost:8000`. You can then use the API endpoints as described in the API Endpoints section below.
+
+### 2. Using the Gradio Interface (Recommended)
+
+The system provides a user-friendly web interface using Gradio, which allows you to:
+- Select and pull `ollama` models directly from the interface
+- Upload and process PDF documents
+- Process web content from URLs
+- Chat with your documents using either local or OpenAI models
+- Toggle Chain of Thought reasoning
+
+To launch the interface:
+
+```bash
+python gradio_app.py
+```
+
+This will start the Gradio server and automatically open the interface in your default browser at `http://localhost:7860`. The interface has two main tabs:
+
+1. **Model Management**:
+ - Download models in advance to prepare them for use
+ - View model information including size and VRAM requirements
+ - Check download status and error messages
+
+2. **Document Processing**:
+ - Upload PDFs using the file uploader
+ - Process web content by entering URLs
+ - View processing status and results
+
+3. **Chat Interface**:
+ - Select between different model options:
+ - Local (Mistral) - Default Mistral-7B model (recommended)
+ - Local (Mistral) with 4-bit or 8-bit quantization for faster inference
+ - Ollama models (llama3, phi-3, qwen2) as alternative options
+ - OpenAI (if API key is configured)
+ - Toggle Chain of Thought reasoning for more detailed responses
+ - Chat with your documents using natural language
+ - Clear chat history as needed
+
+Note: The interface will automatically detect available models based on your configuration:
+- Local Mistral model requires HuggingFace token in `config.yaml` (default option)
+- Ollama models require Ollama to be installed and running (alternative options)
+- OpenAI model requires API key in `.env` file
+
+### 3. Using Individual Python Components via Command Line
+
+#### Process PDFs
+
+To process a PDF file and save the chunks to a JSON file, run:
+
+```bash
+# Process a single PDF
+python pdf_processor.py --input path/to/document.pdf --output chunks.json
+
+# Process multiple PDFs in a directory
+python pdf_processor.py --input path/to/pdf/directory --output chunks.json
+
+# Process a single PDF from a URL
+python pdf_processor.py --input https://example.com/document.pdf --output chunks.json
+# sample pdf: https://arxiv.org/pdf/2203.06605
+```
+
+#### Process Websites with Trafilatura
+
+Process a single website and save the content to a JSON file:
+
+```bash
+python web_processor.py --input https://example.com --output docs/web_content.json
+```
+
+Or, process multiple URLs from a file and save them into a single JSON file:
+
+```bash
+python web_processor.py --input urls.txt --output docs/web_content.json
+```
+
+#### Manage Vector Store
+
+To add documents to the vector store and query them, run:
+
+```bash
+# Add documents from a chunks file, by default to the pdf_collection
+python store.py --add chunks.json
+# for websites, use the --add-web flag
+python store.py --add-web docs/web_content.json
+
+# Query the vector store directly, both pdf and web collections
+# llm will make the best decision on which collection to query based upon your input
+python store.py --query "your search query"
+python local_rag_agent.py --query "your search query"
+```
+
+#### Test Oracle DB Vector Store
+
+The system includes a test script to verify Oracle DB connectivity and examine the contents of your collections. This is useful for:
+- Checking if Oracle DB is properly configured
+- Viewing statistics about your collections
+- Inspecting the content stored in each collection
+- Testing basic vector search functionality
+
+To run the test:
+
+```bash
+# Basic test - checks connection and runs a test query
+python test_oradb.py
+
+# Show only collection statistics without inserting test data
+python test_oradb.py --stats-only
+
+# Specify a custom query for testing
+python test_oradb.py --query "artificial intelligence"
+```
+
+The script will:
+1. Verify Oracle DB credentials in your `config.yaml` file
+2. Test connection to the Oracle DB
+3. Display the total number of chunks in each collection (PDF, Web, Repository, General Knowledge)
+4. Show content and metadata from the most recently inserted chunk in each collection
+5. Unless running with `--stats-only`, insert test data and run a sample vector search
+
+Requirements:
+- Oracle DB credentials properly configured in `config.yaml`:
+ ```yaml
+ ORACLE_DB_USERNAME: ADMIN
+ ORACLE_DB_PASSWORD: your_password_here
+ ORACLE_DB_DSN: your_connection_string_here
+ ```
+- The `oracledb` Python package installed
+
+#### Use RAG Agent
+
+To query documents using either OpenAI or a local model, run:
+
+```bash
+# Using OpenAI (requires API key in .env)
+python rag_agent.py --query "Can you explain the DaGAN Approach proposed in the Depth-Aware Generative Adversarial Network for Talking Head Video Generation article?"
+
+# Using local Mistral model
+python local_rag_agent.py --query "Can you explain the DaGAN Approach proposed in the Depth-Aware Generative Adversarial Network for Talking Head Video Generation article?"
+```
+
+### 4. Complete Pipeline Example
+
+First, we process a document and query it using the local model. Then, we add the document to the vector store and query from the knowledge base to get the RAG system in action.
+
+```bash
+# 1. Process the PDF
+python pdf_processor.py --input example.pdf --output chunks.json
+
+#python pdf_processor.py --input https://arxiv.org/pdf/2203.06605 --output chunks.json
+
+# 2. Add to vector store
+python store.py --add chunks.json
+
+# 3. Query using local model
+python local_rag_agent.py --query "Can you explain the DaGAN Approach proposed in the Depth-Aware Generative Adversarial Network for Talking Head Video Generation article?"
+
+# Or using OpenAI (requires API key):
+python rag_agent.py --query "Can you explain the DaGAN Approach proposed in the Depth-Aware Generative Adversarial Network for Talking Head Video Generation article?"
+```
+
+## 2. Chain of Thought (CoT) Support
+
+The system implements an advanced multi-agent Chain of Thought system, allowing complex queries to be broken down and processed through multiple specialized agents. This feature enhances the reasoning capabilities of both local and cloud-based models.
+
+### Multi-Agent System
+
+The CoT system consists of four specialized agents:
+
+1. **Planner Agent**: Breaks down complex queries into clear, manageable steps
+2. **Research Agent**: Gathers and analyzes relevant information from knowledge bases
+3. **Reasoning Agent**: Applies logical analysis to information and draws conclusions
+4. **Synthesis Agent**: Combines multiple pieces of information into a coherent response
+
+### Using CoT
+
+You can activate the multi-agent CoT system in several ways:
+
+1. **Command Line**:
+```bash
+# Using local Mistral model (default)
+python local_rag_agent.py --query "your query" --use-cot
+
+# Using OpenAI model
+python rag_agent.py --query "your query" --use-cot
+```
+
+2. **Testing the System**:
+```bash
+# Test with local model (default)
+python tests/test_new_cot.py
+
+# Test with OpenAI model
+python tests/test_new_cot.py --model openai
+```
+
+3. **API Endpoint**:
+```http
+POST /query
+Content-Type: application/json
+
+{
+ "query": "your query",
+ "use_cot": true
+}
+```
+
+### Example Output
+
+When CoT is enabled, the system will show:
+- The initial plan for answering the query
+- Research findings for each step
+- Reasoning process and conclusions
+- Final synthesized answer
+- Sources used from the knowledge base
+
+Example:
+```
+Step 1: Planning
+- Break down the technical components
+- Identify key features
+- Analyze implementation details
+
+Step 2: Research
+[Research findings for each step...]
+
+Step 3: Reasoning
+[Logical analysis and conclusions...]
+
+Final Answer:
+[Comprehensive response synthesized from all steps...]
+
+Sources used:
+- document.pdf (pages: 1, 2, 3)
+- implementation.py
+```
+
+### Benefits
+
+The multi-agent CoT approach offers several advantages:
+- More structured and thorough analysis of complex queries
+- Better integration with knowledge bases
+- Transparent reasoning process
+- Improved answer quality through specialized agents
+- Works with both local and cloud-based models
+
+## Annex: API Endpoints
+
+### Upload PDF
+
+```http
+POST /upload/pdf
+Content-Type: multipart/form-data
+
+file:
+```
+
+This endpoint uploads and processes a PDF file, storing its contents in the vector database.
+
+### Query
+
+```http
+POST /query
+Content-Type: application/json
+
+{
+ "query": "your question here"
+}
+```
+
+This endpoint processes a query through the agentic RAG pipeline and returns a response with context.
+
+## Annex: Architecture
+
+
+
+The system consists of several key components:
+
+1. **PDF Processor**: we use `docling` to extract and chunk text from PDF documents
+2. **Web Processor**: we use `trafilatura` to extract and chunk text from websites
+3. **GitHub Repository Processor**: we use `gitingest` to extract and chunk text from repositories
+4. **Vector Store**: Manages document embeddings and similarity search using `Oracle Database 23ai` (default) or `ChromaDB` (fallback)
+5. **RAG Agent**: Makes intelligent decisions about query routing and response generation
+ - OpenAI Agent: Uses `gpt-4-turbo-preview` for high-quality responses, but requires an OpenAI API key
+ - Local Agent: Uses `Mistral-7B` as an open-source alternative
+6. **FastAPI Server**: Provides REST API endpoints for document upload and querying
+7. **Gradio Interface**: Provides a user-friendly web interface for interacting with the RAG system
+
+The RAG Agent flow is the following:
+
+1. Analyzes query type
+2. Try to find relevant PDF context, regardless of query type
+3. If PDF context is found, use it to generate a response.
+4. If no PDF context is found OR if it's a general knowledge query, use the pre-trained LLM directly
+5. Fall back to a "no information" response only in edge cases.
+
+## Annex: Command Line Usage
+
+You can run the system from the command line using:
+
+```bash
+python local_rag_agent.py --query "Your question here" [options]
+```
+
+### Command Line Arguments
+
+| Argument | Description | Default |
+| --- | --- | --- |
+| `--query` | The query to process | *Required* |
+| `--embeddings` | Select embeddings backend (`oracle` or `chromadb`) | `oracle` |
+| `--model` | Model to use for inference | `mistralai/Mistral-7B-Instruct-v0.2` |
+| `--collection` | Collection to query (PDF, Repository, Web, General) | Auto-determined |
+| `--use-cot` | Enable Chain of Thought reasoning | `False` |
+| `--store-path` | Path to ChromaDB store (if using ChromaDB) | `embeddings` |
+| `--skip-analysis` | Skip query analysis step | `False` |
+| `--verbose` | Show full content of sources | `False` |
+| `--quiet` | Disable verbose logging | `False` |
+
+### Examples
+
+Query using Oracle DB (default):
+```bash
+python local_rag_agent.py --query "How does vector search work?"
+```
+
+Force using ChromaDB:
+```bash
+python local_rag_agent.py --query "How does vector search work?" --embeddings chromadb
+```
+
+Query with Chain of Thought reasoning:
+```bash
+python local_rag_agent.py --query "Explain the difference between RAG and fine-tuning" --use-cot
+```
+
+Query a specific collection:
+```bash
+python local_rag_agent.py --query "How to implement a queue?" --collection "Repository Collection"
+```
+
+## Contributing
+
+This project is open source. Please submit your contributions by forking this repository and submitting a pull request! Oracle appreciates any contributions that are made by the open source community.
+
+## License
+
+Copyright (c) 2024 Oracle and/or its affiliates.
+
+Licensed under the Universal Permissive License (UPL), Version 1.0.
+
+See [LICENSE](../LICENSE) for more details.
+
+ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK.
\ No newline at end of file
diff --git a/financial/ai-agents/agents/agent_factory.py b/financial/ai-agents/agents/agent_factory.py
new file mode 100644
index 00000000..5f518673
--- /dev/null
+++ b/financial/ai-agents/agents/agent_factory.py
@@ -0,0 +1,212 @@
+from typing import List, Dict, Any
+from pydantic import BaseModel, Field
+from langchain_openai import ChatOpenAI
+from langchain.prompts import ChatPromptTemplate
+import logging
+import warnings
+from transformers import logging as transformers_logging
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s | %(name)s | %(levelname)s | %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+)
+logger = logging.getLogger(__name__)
+
+# Suppress specific transformers warnings
+transformers_logging.set_verbosity_error()
+warnings.filterwarnings("ignore", message="Setting `pad_token_id` to `eos_token_id`")
+
+class Agent(BaseModel):
+ """Base agent class with common properties"""
+ name: str
+ role: str
+ description: str
+ llm: Any = Field(description="Language model for the agent")
+
+ def log_prompt(self, prompt: str, prefix: str = ""):
+ """Log a prompt being sent to the LLM"""
+ # Check if the prompt contains context
+ if "Context:" in prompt:
+ # Split the prompt at "Context:" and keep only the first part
+ parts = prompt.split("Context:")
+ # Keep the first part and add a note that context is omitted
+ truncated_prompt = parts[0] + "Context: [Context omitted for brevity]"
+ if len(parts) > 2 and "Key Findings:" in parts[1]:
+ # For researcher prompts, keep the "Key Findings:" part
+ key_findings_part = parts[1].split("Key Findings:")
+ if len(key_findings_part) > 1:
+ truncated_prompt += "\nKey Findings:" + key_findings_part[1]
+ logger.info(f"\n{'='*80}\n{prefix} Prompt:\n{'-'*40}\n{truncated_prompt}\n{'='*80}")
+ else:
+ # If no context, log the full prompt
+ logger.info(f"\n{'='*80}\n{prefix} Prompt:\n{'-'*40}\n{prompt}\n{'='*80}")
+
+ def log_response(self, response: str, prefix: str = ""):
+ """Log a response received from the LLM"""
+ # Log the response but truncate if it's too long
+ if len(response) > 500:
+ truncated_response = response[:500] + "... [response truncated]"
+ logger.info(f"\n{'='*80}\n{prefix} Response:\n{'-'*40}\n{truncated_response}\n{'='*80}")
+ else:
+ logger.info(f"\n{'='*80}\n{prefix} Response:\n{'-'*40}\n{response}\n{'='*80}")
+
+class PlannerAgent(Agent):
+ """Agent responsible for breaking down problems and planning steps"""
+ def __init__(self, llm):
+ super().__init__(
+ name="Planner",
+ role="Strategic Planner",
+ description="Breaks down complex problems into manageable steps",
+ llm=llm
+ )
+
+ def plan(self, query: str, context: List[Dict[str, Any]] = None) -> str:
+ logger.info(f"\n🎯 Planning step for query: {query}")
+
+ if context:
+ template = """As a strategic planner, break down this problem into 3-4 clear steps.
+
+ Context: {context}
+ Query: {query}
+
+ Steps:"""
+ context_str = "\n\n".join([f"Context {i+1}:\n{item['content']}" for i, item in enumerate(context)])
+ logger.info(f"Using context ({len(context)} items)")
+ else:
+ template = """As a strategic planner, break down this problem into 3-4 clear steps.
+
+ Query: {query}
+
+ Steps:"""
+ context_str = ""
+ logger.info("No context available")
+
+ prompt = ChatPromptTemplate.from_template(template)
+ messages = prompt.format_messages(query=query, context=context_str)
+ prompt_text = "\n".join([msg.content for msg in messages])
+ self.log_prompt(prompt_text, "Planner")
+
+ response = self.llm.invoke(messages)
+ self.log_response(response.content, "Planner")
+ return response.content
+
+class ResearchAgent(Agent):
+ """Agent responsible for gathering and analyzing information"""
+ vector_store: Any = Field(description="Vector store for searching")
+
+ def __init__(self, llm, vector_store):
+ super().__init__(
+ name="Researcher",
+ role="Information Gatherer",
+ description="Gathers and analyzes relevant information from knowledge bases",
+ llm=llm,
+ vector_store=vector_store
+ )
+
+ def research(self, query: str, step: str) -> List[Dict[str, Any]]:
+ logger.info(f"\n🔍 Researching for step: {step}")
+
+ # Query all collections
+ pdf_results = self.vector_store.query_pdf_collection(query)
+ repo_results = self.vector_store.query_repo_collection(query)
+
+ # Combine results
+ all_results = pdf_results + repo_results
+ logger.info(f"Found {len(all_results)} relevant documents")
+
+ if not all_results:
+ logger.warning("No relevant documents found")
+ return []
+
+ template = """Extract and summarize key information relevant to this step.
+
+ Step: {step}
+ Context: {context}
+
+ Key Findings:"""
+
+ # Create context string but don't log it
+ context_str = "\n\n".join([f"Source {i+1}:\n{item['content']}" for i, item in enumerate(all_results)])
+ prompt = ChatPromptTemplate.from_template(template)
+ messages = prompt.format_messages(step=step, context=context_str)
+ prompt_text = "\n".join([msg.content for msg in messages])
+ self.log_prompt(prompt_text, "Researcher")
+
+ response = self.llm.invoke(messages)
+ self.log_response(response.content, "Researcher")
+
+ return [{"content": response.content, "metadata": {"source": "Research Summary"}}]
+
+class ReasoningAgent(Agent):
+ """Agent responsible for logical reasoning and analysis"""
+ def __init__(self, llm):
+ super().__init__(
+ name="Reasoner",
+ role="Logic and Analysis",
+ description="Applies logical reasoning to information and draws conclusions",
+ llm=llm
+ )
+
+ def reason(self, query: str, step: str, context: List[Dict[str, Any]]) -> str:
+ logger.info(f"\n🤔 Reasoning about step: {step}")
+
+ template = """Analyze the information and draw a clear conclusion for this step.
+
+ Step: {step}
+ Context: {context}
+ Query: {query}
+
+ Conclusion:"""
+
+ # Create context string but don't log it
+ context_str = "\n\n".join([f"Context {i+1}:\n{item['content']}" for i, item in enumerate(context)])
+ prompt = ChatPromptTemplate.from_template(template)
+ messages = prompt.format_messages(step=step, query=query, context=context_str)
+ prompt_text = "\n".join([msg.content for msg in messages])
+ self.log_prompt(prompt_text, "Reasoner")
+
+ response = self.llm.invoke(messages)
+ self.log_response(response.content, "Reasoner")
+ return response.content
+
+class SynthesisAgent(Agent):
+ """Agent responsible for combining information and generating final response"""
+ def __init__(self, llm):
+ super().__init__(
+ name="Synthesizer",
+ role="Information Synthesizer",
+ description="Combines multiple pieces of information into a coherent response",
+ llm=llm
+ )
+
+ def synthesize(self, query: str, reasoning_steps: List[str]) -> str:
+ logger.info(f"\n📝 Synthesizing final answer from {len(reasoning_steps)} reasoning steps")
+
+ template = """As an AI-powered investment assistant, your goal is to generate a personalized investor report that provides tailored insights based on the user’s financial goals, risk tolerance, and real-time market conditions. Ensure recommendations are data-driven, explainable, and compliant with financial regulations. Highlight key risks, opportunities, and portfolio optimization strategies in a professional, structured format.
+
+
+ Query: {query}
+ Steps: {steps}
+
+ Answer:"""
+
+ steps_str = "\n\n".join([f"Step {i+1}:\n{step}" for i, step in enumerate(reasoning_steps)])
+ prompt = ChatPromptTemplate.from_template(template)
+ messages = prompt.format_messages(query=query, steps=steps_str)
+ prompt_text = "\n".join([msg.content for msg in messages])
+ self.log_prompt(prompt_text, "Synthesizer")
+
+ response = self.llm.invoke(messages)
+ self.log_response(response.content, "Synthesizer")
+ return response.content
+
+def create_agents(llm, vector_store=None):
+ """Create and return the set of specialized agents"""
+ return {
+ "planner": PlannerAgent(llm),
+ "researcher": ResearchAgent(llm, vector_store) if vector_store else None,
+ "reasoner": ReasoningAgent(llm),
+ "synthesizer": SynthesisAgent(llm)
+ }
\ No newline at end of file
diff --git a/financial/ai-agents/articles/kubernetes_rag.md b/financial/ai-agents/articles/kubernetes_rag.md
new file mode 100644
index 00000000..d17b5462
--- /dev/null
+++ b/financial/ai-agents/articles/kubernetes_rag.md
@@ -0,0 +1,191 @@
+# Agentic RAG: Enterprise-Scale Multi-Agent AI System on Oracle Cloud Infrastructure
+
+## Introduction
+
+
+
+Agentic RAG is an advanced Retrieval-Augmented Generation system that employs a multi-agent architecture with Chain-of-Thought reasoning, designed for enterprise-scale deployment on Oracle Cloud Infrastructure (OCI).
+
+The system leverages specialized AI agents for complex document analysis and query processing, while taking advantage of OCI's managed Kubernetes service and security features for production-grade deployment.
+
+With this article, we want to show you how you can get started in a few steps to install and deploy this multi-agent RAG system using Oracle Kubernetes Engine (OKE) and OCI.
+
+## Features
+
+This Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph) is based on the following technologies:
+
+- Oracle Kubernetes Engine (OKE)
+- Oracle Cloud Infrastructure (OCI)
+- `ollama` as the inference server for most Large Language Models (LLMs) available in the solution (`llama3`, `phi3`, `qwen2`)
+- `Mistral-7B` language model, with an optional multi-agent Chain of Thought reasoning
+- `ChromaDB` as vector store and retrieval system
+- `Trafilatura`, `docling` and `gitingest` to extract the content from PDFs and web pages, and have them ready to be used by the RAG system
+- Multi-agent architecture with specialized agents:
+ - Planner Agent: Strategic decomposition of complex queries
+ - Research Agent: Intelligent information retrieval (from vector database)
+ - Reasoning Agent: Logical analysis and conclusion drawing
+ - Synthesis Agent: Comprehensive response generation
+- Support for both cloud-based (OpenAI) and local (Mistral-7B) language models
+- Step-by-step reasoning visualization
+- `Gradio` web interface for easy interaction with the RAG system
+
+There are several benefits to using Containerized LLMs over running the LLMs directly on the cloud instances. For example:
+
+- **Scalability**: you can easily scale the LLM workloads across Kubernetes clusters. In our case, we're deploying the solution with 4 agents in the same cluster, but you could deploy each agent in a different cluster if you wanted to accelerate the Chain-of-Thought reasoning processing time (horizontal scaling). You could also use vertical scaling by adding more resources to the same agent.
+- **Resource Optimization**: you can efficiently allocate GPU and memory resources for each agent
+- **Isolation**: Each agent runs in its own container for better resource management
+- **Version Control**: easily update and rollback LLM versions and configurations
+- **Reproducibility**: have a consistent environment across development and production, which is crucial when you're working with complex LLM applications
+- **Cost Efficiency**: you pay only for the resources you need, and when you're doen with your work, you can simply stop the Kubernetes cluster and you won't be charged for the resources anymore.
+- **Integration**: you can easily integrate the RAG system with other programming languages or frameworks, as we also made available a REST-based API to interact with the system, apart from the standard web interface.
+
+In conclusion, it's really easy to scale your system up and down with Kubernetes, without having to worry about the underlying infrastructure, installation, configuration, etc.
+
+Note that the way we've planned the infrastructure is important because it allows us to:
+1. Scale the `chromadb` vector store system independently
+2. The LLM container can be shared across agents, meaning only deploying the LLM container once, and then using it across all the agents
+3. The `Research Agent` can be scaled separately for parallel document processing, if needed
+4. Memory and GPU resources can be optimized, since there's only one LLM instance running
+
+## Deployment in Kubernetes
+
+We have devised two different ways to deploy in Kubernetes: either through a local or distributed system, each offering its own advantages.
+
+### Local Deployment
+
+This method is the easiest way to implement and deploy. We call it local because every resource is deployed in the same pod. The advantages are the following:
+
+- **Simplicity**: All components run in a single pod, making deployment and management straightforward
+- **Easier debugging**: Troubleshooting is simpler when all logs and components are in one place (we're looking to expand the standard logging mechanism that we have right now with `fluentd`)
+- **Quick setup**: Ideal for testing, development, or smaller-scale deployments
+- **Lower complexity**: No need to configure inter-service communication or network policies like port forwarding or such mechanisms.
+
+### Distributed System Deployment
+
+By decoupling the `ollama` LLM inference system to another pod, we could easily ready our system for **vertical scaling**: if we're ever running out of resources or we need to use a bigger model, we don't have to worry about the other solution components not having enough resources for processing and logging: we can simply scale up our inference pod and connect it via a FastAPI or similar system to allow the Gradio interface to make calls to the model, following a distributed system architecture.
+
+The advantages are:
+
+- **Independent Scaling**: Each component can be scaled according to its specific resource needs
+- **Resource Optimization**: Dedicated resources for compute-intensive LLM inference separate from other components
+- **High Availability**: System remains operational even if individual components fail, and we can have multiple pods running failover LLMs to help us with disaster recovery.
+- **Flexible Model Deployment**: Easily swap or upgrade LLM models without affecting the rest of the system (also, with virtually zero downtime!)
+- **Load Balancing**: Distribute inference requests across multiple LLM pods for better performance, thus allowing concurrent users in our Gradio interface.
+- **Isolation**: Performance issues on the LLM side won't impact the interface
+- **Cost Efficiency**: Allocate expensive GPU resources only where needed (inference) while using cheaper CPU resources for other components (e.g. we use GPU for Chain of Thought reasoning, while keeping a quantized CPU LLM for standard chatting).
+
+## Quick Start
+
+For this solution, we have currently implemented the local system deployment, which is what we'll cover in this section.
+
+First, we need to create a GPU OKE cluster with `zx` and Terraform. For this, you can follow the steps in [this repository](https://github.com/vmleon/oci-oke-gpu), or reuse your own Kubernetes cluster if you happen to already have one.
+
+Then, we can start setting up the solution in our cluster by following these steps.
+
+1. Clone the repository containing the Kubernetes manifests:
+
+ ```bash
+ git clone https://github.com/oracle-devrel/devrel-labs.git
+ cd devrel-labs/agentic_rag/k8s
+ ```
+
+2. Create a namespace:
+
+ ```bash
+ kubectl create namespace agentic-rag
+ ```
+
+3. Create a ConfigMap:
+
+ This step will help our deployment for several reasons:
+
+ 1. **Externalized Configuration**: It separates configuration from application code, following best practices for containerized applications
+ 2. **Environment-specific Settings**: Allows us to maintain different configurations for development, testing, and production environments
+ 3. **Credential Management**: Provides a way to inject API tokens (like Hugging Face) without hardcoding them in the image
+ 4. **Runtime Configuration**: Enables changing configuration without rebuilding or redeploying the application container
+ 5. **Consistency**: Ensures all pods use the same configuration when scaled horizontally
+
+ In our specific case, the ConfigMap stores the Hugging Face Hub token for accessing (and downloading) the `mistral-7b` model (and CPU-quantized variants)
+ - Optionally, OpenAI API keys if using those models
+ - Any other environment-specific variables needed by the application, in case we want to make further development and increase the capabilities of the system with external API keys, authentication tokens... etc.
+
+ Let's run the following command to create the config map:
+
+ ```bash
+ # With a Hugging Face token
+ cat <`.
+
+## Resource Requirements
+
+The deployment of this solution requires the following minimum resources:
+
+- **CPU**: 4+ cores
+- **Memory**: 16GB+ RAM
+- **Storage**: 50GB+
+- **GPU**: recommended for faster inference. In theory, you can use `mistral-7b` CPU-quantized models, but it will be sub-optimal.
+
+## Conclusion
+
+You can check out the full AI solution and the deployment options we mention in this article in [the official GitHub repository](https://github.com/oracle-devrel/devrel-labs/tree/main/agentic_rag).
\ No newline at end of file
diff --git a/financial/ai-agents/config_example.yaml b/financial/ai-agents/config_example.yaml
new file mode 100644
index 00000000..368bfb50
--- /dev/null
+++ b/financial/ai-agents/config_example.yaml
@@ -0,0 +1,10 @@
+HUGGING_FACE_HUB_TOKEN: your_token_here
+
+# Oracle DB Configuration
+ORACLE_DB_USERNAME: ADMIN
+ORACLE_DB_PASSWORD: your_password_here
+ORACLE_DB_DSN: >-
+ (description= (retry_count=20)(retry_delay=3)
+ (address=(protocol=tcps)(port=1522)(host=your-oracle-db-host.com))
+ (connect_data=(service_name=your-service-name))
+ (security=(ssl_server_dn_match=yes)))
\ No newline at end of file
diff --git a/financial/ai-agents/docs/oracle_db_integration.md b/financial/ai-agents/docs/oracle_db_integration.md
new file mode 100644
index 00000000..0732a055
--- /dev/null
+++ b/financial/ai-agents/docs/oracle_db_integration.md
@@ -0,0 +1,102 @@
+# Oracle DB 23ai Integration
+
+The Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph) now supports Oracle DB 23ai as a vector store backend, providing enhanced performance, scalability, and enterprise-grade database features.
+
+## Overview
+
+Oracle Database 23ai is used as the default vector storage system when available, with ChromaDB serving as a fallback option. This integration leverages Oracle's vector database capabilities for efficient semantic search and retrieval.
+
+## Requirements
+
+To use the Oracle DB integration, you need:
+
+1. **Oracle Database 23ai**: With vector extensions enabled
+2. **Python Packages**:
+ - `oracledb`: For database connectivity
+ - `sentence-transformers`: For generating embeddings
+
+## Installation
+
+1. Install the required packages:
+
+```bash
+pip install oracledb sentence-transformers
+```
+
+2. Configure your Oracle Database connection in `config.yaml`:
+
+```yaml
+# Oracle DB Configuration
+ORACLE_DB_USERNAME: ADMIN
+ORACLE_DB_PASSWORD: your_password_here
+ORACLE_DB_DSN: >-
+ (description= (retry_count=20)(retry_delay=3)
+ (address=(protocol=tcps)(port=1522)(host=your-oracle-db-host.com))
+ (connect_data=(service_name=your-service-name))
+ (security=(ssl_server_dn_match=yes)))
+```
+
+The system will automatically look for these credentials in your `config.yaml` file. If not found, it will raise an error and fall back to ChromaDB.
+
+## How It Works
+
+The system automatically determines which database to use:
+
+1. First tries to connect to Oracle DB 23ai
+2. If connection succeeds, uses Oracle for all vector operations
+3. If Oracle DB is unavailable, falls back to ChromaDB
+
+## Database Structure
+
+The Oracle DB integration creates the following tables:
+
+- `PDFCollection`: Stores chunks from PDF documents
+- `WebCollection`: Stores chunks from web content
+- `RepoCollection`: Stores chunks from code repositories
+- `GeneralCollection`: Stores general knowledge chunks
+
+Each table has the following structure:
+- `id`: Primary key identifier
+- `text`: The text content of the chunk
+- `metadata`: JSON string containing metadata (source, page, etc.)
+- `embedding`: Vector representation of the text
+
+## Testing
+
+You can test the Oracle DB integration using:
+
+```bash
+python test_oradb.py
+```
+
+Or test both systems using:
+
+```bash
+./test_db_systems.sh
+```
+
+## Switching Between Databases
+
+You can force the system to use ChromaDB instead of Oracle DB by setting the `use_oracle_db` parameter to `False`:
+
+```python
+agent = LocalRAGAgent(use_oracle_db=False)
+```
+
+## Gradio Interface
+
+The Gradio web interface displays which database system is active at the top of the page:
+
+- Green banner: Oracle DB 23ai is active
+- Red banner: ChromaDB is being used (Oracle DB not available)
+
+## Troubleshooting
+
+If you encounter database connection issues:
+
+1. Verify your Oracle DB credentials and connection string
+2. Check that the Oracle DB 23ai instance is running
+3. Ensure you have the required Python packages installed
+4. Check network connectivity to the database server
+
+If Oracle DB connection fails, the system will automatically fall back to ChromaDB without requiring any user intervention.
\ No newline at end of file
diff --git a/financial/ai-agents/gradio_app.py b/financial/ai-agents/gradio_app.py
new file mode 100644
index 00000000..b8a78da3
--- /dev/null
+++ b/financial/ai-agents/gradio_app.py
@@ -0,0 +1,654 @@
+import gradio as gr
+import os
+from typing import List, Dict, Any
+from pathlib import Path
+import tempfile
+from dotenv import load_dotenv
+import yaml
+import torch
+import time
+
+from pdf_processor import PDFProcessor
+from web_processor import WebProcessor
+from repo_processor import RepoProcessor
+from store import VectorStore
+
+# Try to import OraDBVectorStore
+try:
+ from OraDBVectorStore import OraDBVectorStore
+ ORACLE_DB_AVAILABLE = True
+except ImportError:
+ ORACLE_DB_AVAILABLE = False
+
+from local_rag_agent import LocalRAGAgent
+from rag_agent import RAGAgent
+
+# Load environment variables and config
+load_dotenv()
+
+def load_config():
+ """Load configuration from config.yaml"""
+ try:
+ with open('config.yaml', 'r') as f:
+ config = yaml.safe_load(f)
+ return config.get('HUGGING_FACE_HUB_TOKEN')
+ except Exception as e:
+ print(f"Error loading config: {str(e)}")
+ return None
+
+# Initialize components
+pdf_processor = PDFProcessor()
+web_processor = WebProcessor()
+repo_processor = RepoProcessor()
+
+# Initialize vector store (prefer Oracle DB if available)
+if ORACLE_DB_AVAILABLE:
+ try:
+ vector_store = OraDBVectorStore()
+ print("Using Oracle DB 23ai for vector storage")
+ except Exception as e:
+ print(f"Error initializing Oracle DB: {str(e)}")
+ print("Falling back to ChromaDB")
+ vector_store = VectorStore()
+else:
+ vector_store = VectorStore()
+ print("Using ChromaDB for vector storage (Oracle DB not available)")
+
+# Initialize agents
+hf_token = load_config()
+openai_key = os.getenv("OPENAI_API_KEY")
+
+# Initialize agents with use_cot=True to ensure CoT is available
+# Default to Ollama qwen2, fall back to Mistral if available
+try:
+ local_agent = LocalRAGAgent(vector_store, model_name="ollama:qwen2", use_cot=True)
+ print("Using Ollama qwen2 as default model")
+except Exception as e:
+ print(f"Could not initialize Ollama qwen2: {str(e)}")
+ local_agent = LocalRAGAgent(vector_store, use_cot=True) if hf_token else None
+ print("Falling back to Local Mistral model" if hf_token else "No local model available")
+
+openai_agent = RAGAgent(vector_store, openai_api_key=openai_key, use_cot=True) if openai_key else None
+
+def process_pdf(file: tempfile._TemporaryFileWrapper) -> str:
+ """Process uploaded PDF file"""
+ try:
+ chunks, document_id = pdf_processor.process_pdf(file.name)
+ vector_store.add_pdf_chunks(chunks, document_id=document_id)
+ return f"✓ Successfully processed PDF and added {len(chunks)} chunks to knowledge base (ID: {document_id})"
+ except Exception as e:
+ return f"✗ Error processing PDF: {str(e)}"
+
+def process_url(url: str) -> str:
+ """Process web content from URL"""
+ try:
+ # Process URL and get chunks
+ chunks = web_processor.process_url(url)
+ if not chunks:
+ return "✗ No content extracted from URL"
+
+ # Add chunks to vector store with URL as source ID
+ vector_store.add_web_chunks(chunks, source_id=url)
+ return f"✓ Successfully processed URL and added {len(chunks)} chunks to knowledge base"
+ except Exception as e:
+ return f"✗ Error processing URL: {str(e)}"
+
+def process_repo(repo_path: str) -> str:
+ """Process repository content"""
+ try:
+ # Process repository and get chunks
+ chunks, document_id = repo_processor.process_repo(repo_path)
+ if not chunks:
+ return "✗ No content extracted from repository"
+
+ # Add chunks to vector store
+ vector_store.add_repo_chunks(chunks, document_id=document_id)
+ return f"✓ Successfully processed repository and added {len(chunks)} chunks to knowledge base (ID: {document_id})"
+ except Exception as e:
+ return f"✗ Error processing repository: {str(e)}"
+
+def chat(message: str, history: List[List[str]], agent_type: str, use_cot: bool, collection: str) -> List[List[str]]:
+ """Process chat message using selected agent and collection"""
+ try:
+ print("\n" + "="*50)
+ print(f"New message received: {message}")
+ print(f"Agent: {agent_type}, CoT: {use_cot}, Collection: {collection}")
+ print("="*50 + "\n")
+
+ # Determine if we should skip analysis based on collection and interface type
+ # Skip analysis for General Knowledge or when using standard chat interface (not CoT)
+ skip_analysis = collection == "General Knowledge" or not use_cot
+
+ # Map collection names to actual collection names in vector store
+ collection_mapping = {
+ "PDF Collection": "pdf_documents",
+ "Repository Collection": "repository_documents",
+ "Web Knowledge Base": "web_documents",
+ "General Knowledge": "general_knowledge"
+ }
+
+ # Get the actual collection name
+ actual_collection = collection_mapping.get(collection, "pdf_documents")
+
+ # Parse agent type to determine model and quantization
+ quantization = None
+ model_name = None
+
+ if "4-bit" in agent_type:
+ quantization = "4bit"
+ model_type = "Local (Mistral)"
+ elif "8-bit" in agent_type:
+ quantization = "8bit"
+ model_type = "Local (Mistral)"
+ elif "Ollama" in agent_type:
+ model_type = "Ollama"
+ # Extract model name from agent_type and use correct Ollama model names
+ if "llama3" in agent_type.lower():
+ model_name = "ollama:llama3"
+ elif "phi-3" in agent_type.lower():
+ model_name = "ollama:phi3"
+ elif "qwen2" in agent_type.lower():
+ model_name = "ollama:qwen2"
+ else:
+ model_type = agent_type
+
+ # Select appropriate agent and reinitialize with correct settings
+ if "Local" in model_type:
+ # For HF models, we need the token
+ if not hf_token:
+ response_text = "Local agent not available. Please check your HuggingFace token configuration."
+ print(f"Error: {response_text}")
+ return history + [[message, response_text]]
+ agent = LocalRAGAgent(vector_store, use_cot=use_cot, collection=collection,
+ skip_analysis=skip_analysis, quantization=quantization)
+ elif model_type == "Ollama":
+ # For Ollama models
+ if model_name:
+ try:
+ agent = LocalRAGAgent(vector_store, model_name=model_name, use_cot=use_cot,
+ collection=collection, skip_analysis=skip_analysis)
+ except Exception as e:
+ response_text = f"Error initializing Ollama model: {str(e)}. Falling back to Local Mistral."
+ print(f"Error: {response_text}")
+ # Fall back to Mistral if Ollama fails
+ if hf_token:
+ agent = LocalRAGAgent(vector_store, use_cot=use_cot, collection=collection,
+ skip_analysis=skip_analysis)
+ else:
+ return history + [[message, "Local Mistral agent not available for fallback. Please check your HuggingFace token configuration."]]
+ else:
+ response_text = "Ollama model not specified correctly."
+ print(f"Error: {response_text}")
+ return history + [[message, response_text]]
+ else:
+ if not openai_key:
+ response_text = "OpenAI agent not available. Please check your OpenAI API key configuration."
+ print(f"Error: {response_text}")
+ return history + [[message, response_text]]
+ agent = RAGAgent(vector_store, openai_api_key=openai_key, use_cot=use_cot,
+ collection=collection, skip_analysis=skip_analysis)
+
+ # Process query and get response
+ print("Processing query...")
+ response = agent.process_query(message)
+ print("Query processed successfully")
+
+ # Format response with reasoning steps if CoT is enabled
+ if use_cot and "reasoning_steps" in response:
+ formatted_response = "🤔 Let me think about this step by step:\n\n"
+ print("\nChain of Thought Reasoning Steps:")
+ print("-" * 50)
+
+ # Add each reasoning step with conclusion
+ for i, step in enumerate(response["reasoning_steps"], 1):
+ step_text = f"Step {i}:\n{step}\n"
+ formatted_response += step_text
+ print(step_text)
+
+ # Add intermediate response to chat history to show progress
+ history.append([None, f"🔄 Step {i} Conclusion:\n{step}"])
+
+ # Add final answer
+ print("\nFinal Answer:")
+ print("-" * 50)
+ final_answer = "\n🎯 Final Answer:\n" + response["answer"]
+ formatted_response += final_answer
+ print(final_answer)
+
+ # Add sources if available
+ if response.get("context"):
+ print("\nSources Used:")
+ print("-" * 50)
+ sources_text = "\n📚 Sources used:\n"
+ formatted_response += sources_text
+ print(sources_text)
+
+ for ctx in response["context"]:
+ source = ctx["metadata"].get("source", "Unknown")
+ if "page_numbers" in ctx["metadata"]:
+ pages = ctx["metadata"].get("page_numbers", [])
+ source_line = f"- {source} (pages: {pages})\n"
+ else:
+ file_path = ctx["metadata"].get("file_path", "Unknown")
+ source_line = f"- {source} (file: {file_path})\n"
+ formatted_response += source_line
+ print(source_line)
+
+ # Add final formatted response to history
+ history.append([message, formatted_response])
+ else:
+ # For standard response (no CoT)
+ formatted_response = response["answer"]
+ print("\nStandard Response:")
+ print("-" * 50)
+ print(formatted_response)
+
+ # Add sources if available
+ if response.get("context"):
+ print("\nSources Used:")
+ print("-" * 50)
+ sources_text = "\n\n📚 Sources used:\n"
+ formatted_response += sources_text
+ print(sources_text)
+
+ for ctx in response["context"]:
+ source = ctx["metadata"].get("source", "Unknown")
+ if "page_numbers" in ctx["metadata"]:
+ pages = ctx["metadata"].get("page_numbers", [])
+ source_line = f"- {source} (pages: {pages})\n"
+ else:
+ file_path = ctx["metadata"].get("file_path", "Unknown")
+ source_line = f"- {source} (file: {file_path})\n"
+ formatted_response += source_line
+ print(source_line)
+
+ history.append([message, formatted_response])
+
+ print("\n" + "="*50)
+ print("Response complete")
+ print("="*50 + "\n")
+
+ return history
+ except Exception as e:
+ error_msg = f"Error processing query: {str(e)}"
+ print(f"\nError occurred:")
+ print("-" * 50)
+ print(error_msg)
+ print("="*50 + "\n")
+ history.append([message, error_msg])
+ return history
+
+def create_interface():
+ """Create Gradio interface"""
+ with gr.Blocks(title="Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph)", theme=gr.themes.Soft()) as interface:
+ gr.Markdown("""
+ # Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph)
+
+ """)
+
+ # Show Oracle DB status
+ if ORACLE_DB_AVAILABLE and hasattr(vector_store, 'connection'):
+ gr.Markdown("""
+
+ ✅ Oracle DB 23ai is active and being used for vector and private data search.
+
+ """)
+ else:
+ gr.Markdown("""
+
+ ✅ ChromeDB is active and being used for vector storage.
+
+ """)
+
+ # Create model choices list for reuse
+ model_choices = []
+ # HF models first if token is available
+ if hf_token:
+ model_choices.extend([
+ "Local (Mistral)",
+ "Local (Mistral) - 4-bit Quantized",
+ "Local (Mistral) - 8-bit Quantized",
+ ])
+ # Then Ollama models (don't require HF token)
+ model_choices.extend([
+ "Ollama - llama3",
+ "Ollama - phi-3",
+ "Ollama - qwen2"
+ ])
+ if openai_key:
+ model_choices.append("OpenAI")
+
+ # Set default model to Ollama - qwen2
+ default_model = "Ollama - qwen2"
+
+ # Model Management Tab (First Tab)
+ with gr.Tab("Model Management"):
+ gr.Markdown("""
+
+ """)
+
+ with gr.Row():
+ with gr.Column():
+ model_dropdown = gr.Dropdown(
+ choices=model_choices,
+ value=default_model if default_model in model_choices else model_choices[0] if model_choices else None,
+ label="Select Model to Download",
+ interactive=True
+ )
+ download_button = gr.Button("Download Selected Model")
+ model_status = gr.Textbox(
+ label="Download Status",
+ placeholder="Select a model and click Download to begin...",
+ interactive=False
+ )
+
+ with gr.Column():
+ gr.Markdown("""
+
+
+ """)
+
+ # Document Processing Tab
+ with gr.Tab("Document Processing"):
+ with gr.Row():
+ with gr.Column():
+ pdf_file = gr.File(label="Upload PDF")
+ pdf_button = gr.Button("Process PDF")
+ pdf_output = gr.Textbox(label="PDF Processing Output")
+
+ with gr.Column():
+ url_input = gr.Textbox(label="Enter URL")
+ url_button = gr.Button("Process URL")
+ url_output = gr.Textbox(label="URL Processing Output")
+
+ with gr.Column():
+ repo_input = gr.Textbox(label="Enter Repository Path or URL")
+ repo_button = gr.Button("Process Repository")
+ repo_output = gr.Textbox(label="Repository Processing Output")
+
+ # Define collection choices once to ensure consistency
+ collection_choices = [
+ "PDF Collection",
+ "Repository Collection",
+ "Web Knowledge Base",
+ "General Knowledge"
+ ]
+
+ with gr.Tab("Standard Chat Interface"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ standard_agent_dropdown = gr.Dropdown(
+ choices=model_choices,
+ value=default_model if default_model in model_choices else model_choices[0] if model_choices else None,
+ label="Select Agent"
+ )
+ with gr.Column(scale=1):
+ standard_collection_dropdown = gr.Dropdown(
+ choices=collection_choices,
+ value=collection_choices[0],
+ label="Select Knowledge Base",
+ info="Choose which knowledge base to use for answering questions"
+ )
+ gr.Markdown("""
+
+ """)
+ standard_chatbot = gr.Chatbot(height=400)
+ with gr.Row():
+ standard_msg = gr.Textbox(label="Your Message", scale=9)
+ standard_send = gr.Button("Send", scale=1)
+ standard_clear = gr.Button("Clear Chat")
+
+ with gr.Tab("Chain of Thought Chat Interface"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ cot_agent_dropdown = gr.Dropdown(
+ choices=model_choices,
+ value=default_model if default_model in model_choices else model_choices[0] if model_choices else None,
+ label="Select Agent"
+ )
+ with gr.Column(scale=1):
+ cot_collection_dropdown = gr.Dropdown(
+ choices=collection_choices,
+ value=collection_choices[0],
+ label="Select Knowledge Base",
+ info="Choose which knowledge base to use for answering questions"
+ )
+ gr.Markdown("""
+
+ """)
+ cot_chatbot = gr.Chatbot(height=400)
+ with gr.Row():
+ cot_msg = gr.Textbox(label="Your Message", scale=9)
+ cot_send = gr.Button("Send", scale=1)
+ cot_clear = gr.Button("Clear Chat")
+
+ # Event handlers
+ pdf_button.click(process_pdf, inputs=[pdf_file], outputs=[pdf_output])
+ url_button.click(process_url, inputs=[url_input], outputs=[url_output])
+ repo_button.click(process_repo, inputs=[repo_input], outputs=[repo_output])
+
+ # Model download event handler
+ download_button.click(download_model, inputs=[model_dropdown], outputs=[model_status])
+
+ # Standard chat handlers
+ standard_msg.submit(
+ chat,
+ inputs=[
+ standard_msg,
+ standard_chatbot,
+ standard_agent_dropdown,
+ gr.State(False), # use_cot=False
+ standard_collection_dropdown
+ ],
+ outputs=[standard_chatbot]
+ )
+ standard_send.click(
+ chat,
+ inputs=[
+ standard_msg,
+ standard_chatbot,
+ standard_agent_dropdown,
+ gr.State(False), # use_cot=False
+ standard_collection_dropdown
+ ],
+ outputs=[standard_chatbot]
+ )
+ standard_clear.click(lambda: None, None, standard_chatbot, queue=False)
+
+ # CoT chat handlers
+ cot_msg.submit(
+ chat,
+ inputs=[
+ cot_msg,
+ cot_chatbot,
+ cot_agent_dropdown,
+ gr.State(True), # use_cot=True
+ cot_collection_dropdown
+ ],
+ outputs=[cot_chatbot]
+ )
+ cot_send.click(
+ chat,
+ inputs=[
+ cot_msg,
+ cot_chatbot,
+ cot_agent_dropdown,
+ gr.State(True), # use_cot=True
+ cot_collection_dropdown
+ ],
+ outputs=[cot_chatbot]
+ )
+ cot_clear.click(lambda: None, None, cot_chatbot, queue=False)
+
+ # Replace Instructions with an image
+ gr.Markdown("## Personalized Investment Report Generation")
+ gr.Image(value="img/PersonalizedInvestmentReportGeneration.png", label="Instructions Image")
+ gr.Image(value="img/financialaiagentssolution.png", label="Instructions Image")
+
+ return interface
+
+def main():
+ # Check configuration
+ try:
+ import ollama
+ try:
+ # Check if Ollama is running and qwen2 is available
+ models = ollama.list().models
+ available_models = [model.model for model in models]
+ if "qwen2" not in available_models and "qwen2:latest" not in available_models:
+ print("⚠️ Warning: Ollama is running but qwen2 model is not available. Please run 'ollama pull qwen2' or download through the interface.")
+ except Exception:
+ print("⚠️ Warning: Ollama is installed but not running or encountered an error. The default model may not work.")
+ except ImportError:
+ print("⚠️ Warning: Ollama package not installed. Please install with: pip install ollama")
+
+ if not hf_token and not openai_key:
+ print("⚠️ Warning: Neither HuggingFace token nor OpenAI key found. Using Ollama only.")
+
+ # Launch interface
+ interface = create_interface()
+ interface.launch(
+ server_name="0.0.0.0",
+ server_port=8080,
+ share=False,
+ inbrowser=False
+ )
+
+def download_model(model_type: str) -> str:
+ """Download a model and return status message"""
+ try:
+ print(f"Downloading model: {model_type}")
+
+ # Parse model type to determine model and quantization
+ quantization = None
+ model_name = None
+
+ if "4-bit" in model_type or "8-bit" in model_type:
+ # For HF models, we need the token
+ if not hf_token:
+ return "❌ Error: HuggingFace token not found in config.yaml. Please add your token first."
+
+ model_name = "mistralai/Mistral-7B-Instruct-v0.2" # Default model
+ if "4-bit" in model_type:
+ quantization = "4bit"
+ elif "8-bit" in model_type:
+ quantization = "8bit"
+
+ # Start download timer
+ start_time = time.time()
+
+ try:
+ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
+
+ # Download tokenizer first (smaller download to check access)
+ try:
+ tokenizer = AutoTokenizer.from_pretrained(model_name, token=hf_token)
+ except Exception as e:
+ if "401" in str(e):
+ return f"❌ Error: This model is gated. Please accept the terms on the Hugging Face website: https://huggingface.co/{model_name}"
+ else:
+ return f"❌ Error downloading tokenizer: {str(e)}"
+
+ # Set up model loading parameters
+ model_kwargs = {
+ "token": hf_token,
+ "device_map": None, # Don't load on GPU for download only
+ }
+
+ # Apply quantization if specified
+ if quantization == '4bit':
+ try:
+ quantization_config = BitsAndBytesConfig(
+ load_in_4bit=True,
+ bnb_4bit_compute_dtype=torch.float16,
+ bnb_4bit_use_double_quant=True,
+ bnb_4bit_quant_type="nf4"
+ )
+ model_kwargs["quantization_config"] = quantization_config
+ except ImportError:
+ return "❌ Error: bitsandbytes not installed. Please install with: pip install bitsandbytes>=0.41.0"
+ elif quantization == '8bit':
+ try:
+ quantization_config = BitsAndBytesConfig(load_in_8bit=True)
+ model_kwargs["quantization_config"] = quantization_config
+ except ImportError:
+ return "❌ Error: bitsandbytes not installed. Please install with: pip install bitsandbytes>=0.41.0"
+
+ # Download model (but don't load it fully to save memory)
+ AutoModelForCausalLM.from_pretrained(
+ model_name,
+ **model_kwargs
+ )
+
+ # Calculate download time
+ download_time = time.time() - start_time
+ return f"✅ Successfully downloaded {model_type} in {download_time:.1f} seconds."
+
+ except Exception as e:
+ return f"❌ Error downloading model: {str(e)}"
+
+ elif "Ollama" in model_type:
+ # Extract model name from model_type
+ if "llama3" in model_type.lower():
+ model_name = "llama3"
+ elif "phi-3" in model_type.lower():
+ model_name = "phi3"
+ elif "qwen2" in model_type.lower():
+ model_name = "qwen2"
+ else:
+ return "❌ Error: Unknown Ollama model type"
+
+ # Use Ollama to pull the model
+ try:
+ import ollama
+
+ print(f"Pulling Ollama model: {model_name}")
+ start_time = time.time()
+
+ # Check if model already exists
+ try:
+ models = ollama.list().models
+ available_models = [model.model for model in models]
+
+ # Check for model with or without :latest suffix
+ if model_name in available_models or f"{model_name}:latest" in available_models:
+ return f"✅ Model {model_name} is already available in Ollama."
+ except Exception:
+ # If we can't check, proceed with pull anyway
+ pass
+
+ # Pull the model with progress tracking
+ progress_text = ""
+ for progress in ollama.pull(model_name, stream=True):
+ status = progress.get('status')
+ if status:
+ progress_text = f"Status: {status}"
+ print(progress_text)
+
+ # Show download progress
+ if 'completed' in progress and 'total' in progress:
+ completed = progress['completed']
+ total = progress['total']
+ if total > 0:
+ percent = (completed / total) * 100
+ progress_text = f"Downloading: {percent:.1f}% ({completed}/{total})"
+ print(progress_text)
+
+ # Calculate download time
+ download_time = time.time() - start_time
+ return f"✅ Successfully pulled Ollama model {model_name} in {download_time:.1f} seconds."
+
+ except ImportError:
+ return "❌ Error: ollama not installed. Please install with: pip install ollama"
+ except ConnectionError:
+ return "❌ Error: Could not connect to Ollama. Please make sure Ollama is installed and running."
+ except Exception as e:
+ return f"❌ Error pulling Ollama model: {str(e)}"
+ else:
+ return "❌ Error: Unknown model type"
+
+ except Exception as e:
+ return f"❌ Error: {str(e)}"
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/img/PersonalizedInvestmentReportGeneration.png b/financial/ai-agents/img/PersonalizedInvestmentReportGeneration.png
new file mode 100644
index 00000000..1c616876
Binary files /dev/null and b/financial/ai-agents/img/PersonalizedInvestmentReportGeneration.png differ
diff --git a/financial/ai-agents/img/architecture.png b/financial/ai-agents/img/architecture.png
new file mode 100644
index 00000000..e22de006
Binary files /dev/null and b/financial/ai-agents/img/architecture.png differ
diff --git a/financial/ai-agents/img/cot_final_answer.png b/financial/ai-agents/img/cot_final_answer.png
new file mode 100644
index 00000000..47a69dfd
Binary files /dev/null and b/financial/ai-agents/img/cot_final_answer.png differ
diff --git a/financial/ai-agents/img/financialaiagentssolution.png b/financial/ai-agents/img/financialaiagentssolution.png
new file mode 100644
index 00000000..752fc8a8
Binary files /dev/null and b/financial/ai-agents/img/financialaiagentssolution.png differ
diff --git a/financial/ai-agents/img/gradio_1.png b/financial/ai-agents/img/gradio_1.png
new file mode 100644
index 00000000..422e2f89
Binary files /dev/null and b/financial/ai-agents/img/gradio_1.png differ
diff --git a/financial/ai-agents/img/gradio_2.png b/financial/ai-agents/img/gradio_2.png
new file mode 100644
index 00000000..dc31a06b
Binary files /dev/null and b/financial/ai-agents/img/gradio_2.png differ
diff --git a/financial/ai-agents/img/gradio_3.png b/financial/ai-agents/img/gradio_3.png
new file mode 100644
index 00000000..7979ca1b
Binary files /dev/null and b/financial/ai-agents/img/gradio_3.png differ
diff --git a/financial/ai-agents/img/output_1.png b/financial/ai-agents/img/output_1.png
new file mode 100644
index 00000000..7709e992
Binary files /dev/null and b/financial/ai-agents/img/output_1.png differ
diff --git a/financial/ai-agents/img/output_2.png b/financial/ai-agents/img/output_2.png
new file mode 100644
index 00000000..9e368583
Binary files /dev/null and b/financial/ai-agents/img/output_2.png differ
diff --git a/financial/ai-agents/img/output_3.png b/financial/ai-agents/img/output_3.png
new file mode 100644
index 00000000..87be7634
Binary files /dev/null and b/financial/ai-agents/img/output_3.png differ
diff --git a/financial/ai-agents/img/output_response.png b/financial/ai-agents/img/output_response.png
new file mode 100644
index 00000000..ae358e3d
Binary files /dev/null and b/financial/ai-agents/img/output_response.png differ
diff --git a/financial/ai-agents/k8s/README.md b/financial/ai-agents/k8s/README.md
new file mode 100644
index 00000000..d0c70428
--- /dev/null
+++ b/financial/ai-agents/k8s/README.md
@@ -0,0 +1,152 @@
+# Kubernetes Deployment for Agentic RAG
+
+This directory contains Kubernetes manifests and guides for deploying the Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph) on Kubernetes.
+
+## Deployment Options
+
+We currently provide a single deployment option with plans for a distributed deployment in the future:
+
+### Local Deployment
+
+This is a single-pod deployment where all components run in the same pod. It's simpler to deploy and manage, making it ideal for testing and development.
+
+**Features:**
+- Includes both Hugging Face models and Ollama for inference
+- Uses GPU acceleration for faster inference
+- Simpler deployment and management
+- Easier debugging (all logs in one place)
+- Lower complexity (no inter-service communication)
+- Quicker setup
+
+**Model Options:**
+- **Hugging Face Models**: Uses `Mistral-7B` models from Hugging Face (requires a token)
+- **Ollama Models**: Uses `ollama` for inference (llama3, phi3, qwen2)
+
+### Future: Distributed System Deployment
+
+A distributed system deployment that separates the LLM inference system into its own service is planned for future releases. This will allow for better resource allocation and scaling in production environments.
+
+**Advantages:**
+- Independent scaling of components
+- Better resource optimization
+- Higher availability
+- Flexible model deployment
+- Load balancing capabilities
+
+## Deployment Guides
+
+We provide several guides for different environments:
+
+1. [**General Kubernetes Guide**](README_k8s.md): Basic instructions for any Kubernetes cluster
+2. [**Oracle Kubernetes Engine (OKE) Guide**](OKE_DEPLOYMENT.md): Detailed instructions for deploying on OCI
+3. [**Minikube Guide**](MINIKUBE.md): Quick start guide for local testing with Minikube
+
+## Directory Structure
+
+```bash
+k8s/
+├── README_MAIN.md # This file
+├── README.md # General Kubernetes guide
+├── OKE_DEPLOYMENT.md # Oracle Kubernetes Engine guide
+├── MINIKUBE.md # Minikube guide
+├── deploy.sh # Deployment script
+└── local-deployment/ # Manifests for local deployment
+ ├── configmap.yaml
+ ├── deployment.yaml
+ └── service.yaml
+```
+
+## Quick Start
+
+For a quick start, use the deployment script. Just go into the script and replace your `HF_TOKEN` in line 17:
+
+```bash
+# Make the script executable
+chmod +x deploy.sh
+
+# Deploy with a Hugging Face token
+./deploy.sh --hf-token "your-huggingface-token" --namespace agentic-rag
+
+# Or deploy without a Hugging Face token (Ollama models only)
+./deploy.sh --namespace agentic-rag
+
+# Deploy without GPU support (not recommended for production)
+./deploy.sh --cpu-only --namespace agentic-rag
+```
+
+## Resource Requirements
+
+The deployment requires the following minimum resources:
+
+- **CPU**: 4+ cores
+- **Memory**: 16GB+ RAM
+- **Storage**: 50GB+
+- **GPU**: 1 NVIDIA GPU (required for optimal performance)
+
+## Next Steps
+
+After deployment, you can:
+
+1. **Add Documents**: Upload PDFs, process web content, or add repositories to the knowledge base
+2. **Configure Models**: Download and configure different models
+3. **Customize**: Adjust the system to your specific needs
+4. **Scale**: For production use, consider implementing the distributed deployment with persistent storage (coming soon)
+
+## Troubleshooting
+
+See the specific deployment guides for troubleshooting tips. Common issues include:
+
+- Insufficient resources
+- Network connectivity problems
+- Model download failures
+- Configuration errors
+- GPU driver issues
+
+### GPU-Related Issues
+
+If you encounter GPU-related issues:
+
+1. **Check GPU availability**: Ensure your Kubernetes cluster has GPU nodes available
+2. **Verify NVIDIA drivers**: Make sure NVIDIA drivers are installed on the nodes
+3. **Check NVIDIA device plugin**: Ensure the NVIDIA device plugin is installed in your cluster
+4. **Inspect pod logs**: Check for GPU-related errors in the pod logs
+
+```bash
+kubectl logs -f deployment/agentic-rag -n
+```
+
+## GPU Configuration Summary
+
+The deployment has been configured to use GPU acceleration by default for optimal performance:
+
+### Key GPU Configuration Changes
+
+1. **Resource Requests and Limits**:
+ - Each pod requests and is limited to 1 NVIDIA GPU
+ - Memory and CPU resources have been increased to better support GPU workloads
+
+2. **NVIDIA Container Support**:
+ - The deployment installs NVIDIA drivers and CUDA in the container
+ - Environment variables are set to enable GPU visibility and capabilities
+
+3. **Ollama GPU Configuration**:
+ - Ollama is configured to use GPU acceleration automatically
+ - Models like llama3, phi3, and qwen2 will benefit from GPU acceleration
+
+4. **Deployment Script Enhancements**:
+ - Added GPU availability detection
+ - Added `--cpu-only` flag for environments without GPUs
+ - Provides guidance for GPU monitoring and troubleshooting
+
+5. **Documentation Updates**:
+ - Added GPU-specific instructions for different Kubernetes environments
+ - Included troubleshooting steps for GPU-related issues
+ - Updated resource requirements to reflect GPU needs
+
+### CPU Fallback
+
+While the deployment is optimized for GPU usage, a CPU-only mode is available using the `--cpu-only` flag with the deployment script. However, this is not recommended for production use as inference performance will be significantly slower.
+
+## Contributing
+
+Contributions to improve the deployment manifests and guides are welcome. Please submit a pull request or open an issue.
\ No newline at end of file
diff --git a/financial/ai-agents/k8s/deploy.sh b/financial/ai-agents/k8s/deploy.sh
new file mode 100644
index 00000000..bcc77d23
--- /dev/null
+++ b/financial/ai-agents/k8s/deploy.sh
@@ -0,0 +1,122 @@
+#!/bin/bash
+
+# Deployment script for Agentic RAG
+
+# Function to display usage
+usage() {
+ echo "Usage: $0 [--hf-token TOKEN] [--namespace NAMESPACE] [--cpu-only]"
+ echo ""
+ echo "Options:"
+ echo " --hf-token TOKEN Hugging Face token (optional but recommended)"
+ echo " --namespace NAMESPACE Kubernetes namespace to deploy to (default: default)"
+ echo " --cpu-only Deploy without GPU support (not recommended for production)"
+ exit 1
+}
+
+# Default values
+NAMESPACE="default"
+HF_TOKEN=""
+CPU_ONLY=false
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --hf-token)
+ HF_TOKEN="$2"
+ shift 2
+ ;;
+ --namespace)
+ NAMESPACE="$2"
+ shift 2
+ ;;
+ --cpu-only)
+ CPU_ONLY=true
+ shift
+ ;;
+ *)
+ usage
+ ;;
+ esac
+done
+
+# Create namespace if it doesn't exist
+kubectl get namespace $NAMESPACE > /dev/null 2>&1 || kubectl create namespace $NAMESPACE
+
+echo "Deploying Agentic RAG to namespace $NAMESPACE..."
+
+# Check for GPU availability if not in CPU-only mode
+if [[ "$CPU_ONLY" == "false" ]]; then
+ echo "Checking for GPU availability..."
+ GPU_COUNT=$(kubectl get nodes "-o=custom-columns=GPU:.status.allocatable.nvidia\.com/gpu" --no-headers | grep -v "" | wc -l)
+
+ if [[ "$GPU_COUNT" -eq 0 ]]; then
+ echo "WARNING: No GPUs detected in the cluster!"
+ echo "The deployment is configured to use GPUs, but none were found."
+ echo "Options:"
+ echo " 1. Install the NVIDIA device plugin: kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.0/nvidia-device-plugin.yml"
+ echo " 2. Use --cpu-only flag to deploy without GPU support (not recommended for production)"
+ echo " 3. Ensure your nodes have GPUs and proper drivers installed"
+
+ read -p "Continue with deployment anyway? (y/n): " CONTINUE
+ if [[ "$CONTINUE" != "y" && "$CONTINUE" != "Y" ]]; then
+ echo "Deployment aborted."
+ exit 1
+ fi
+
+ echo "Continuing with deployment despite no GPUs detected..."
+ else
+ echo "Found $GPU_COUNT nodes with GPUs available."
+ fi
+fi
+
+# Create ConfigMap with Hugging Face token if provided
+if [[ -n "$HF_TOKEN" ]]; then
+ echo "Using provided Hugging Face token..."
+ cat < local-deployment/deployment-cpu.yaml
+ kubectl apply -n $NAMESPACE -f local-deployment/deployment-cpu.yaml
+ rm local-deployment/deployment-cpu.yaml
+else
+ kubectl apply -n $NAMESPACE -f local-deployment/deployment.yaml
+fi
+
+kubectl apply -n $NAMESPACE -f local-deployment/service.yaml
+
+echo "Deployment started. Check status with: kubectl get pods -n $NAMESPACE"
+echo "Access the application with: kubectl get service agentic-rag -n $NAMESPACE"
+echo "Note: Initial startup may take some time as models are downloaded."
+
+# Provide additional guidance for monitoring GPU usage
+if [[ "$CPU_ONLY" == "false" ]]; then
+ echo ""
+ echo "To monitor GPU usage:"
+ echo " 1. Check pod status: kubectl get pods -n $NAMESPACE"
+ echo " 2. View pod logs: kubectl logs -f deployment/agentic-rag -n $NAMESPACE"
+ echo " 3. Check GPU allocation: kubectl describe pod -l app=agentic-rag -n $NAMESPACE | grep -A5 'Allocated resources'"
+fi
\ No newline at end of file
diff --git a/financial/ai-agents/k8s/local-deployment/configmap.yaml b/financial/ai-agents/k8s/local-deployment/configmap.yaml
new file mode 100644
index 00000000..20cef3c8
--- /dev/null
+++ b/financial/ai-agents/k8s/local-deployment/configmap.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: agentic-rag-config
+data:
+ config.yaml: |
+ HUGGING_FACE_HUB_TOKEN: "your-huggingface-token"
+ # Optional OpenAI configuration
+ # .env: |
+ # OPENAI_API_KEY=your-openai-api-key
\ No newline at end of file
diff --git a/financial/ai-agents/k8s/local-deployment/deployment.yaml b/financial/ai-agents/k8s/local-deployment/deployment.yaml
new file mode 100644
index 00000000..21292635
--- /dev/null
+++ b/financial/ai-agents/k8s/local-deployment/deployment.yaml
@@ -0,0 +1,116 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: agentic-rag
+ labels:
+ app: agentic-rag
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: agentic-rag
+ template:
+ metadata:
+ labels:
+ app: agentic-rag
+ spec:
+ containers:
+ - name: agentic-rag
+ image: python:3.10-slim
+ resources:
+ requests:
+ memory: "8Gi"
+ cpu: "2"
+ ephemeral-storage: "50Gi" # Add this
+ limits:
+ memory: "16Gi"
+ cpu: "4"
+ ephemeral-storage: "100Gi" # Add this
+ ports:
+ - containerPort: 7860
+ name: gradio
+ - containerPort: 11434
+ name: ollama-api
+ volumeMounts:
+ - name: config-volume
+ mountPath: /app/config.yaml
+ subPath: config.yaml
+ - name: data-volume
+ mountPath: /app/embeddings
+ - name: chroma-volume
+ mountPath: /app/chroma_db
+ - name: ollama-models
+ mountPath: /root/.ollama
+ command: ["/bin/bash", "-c"]
+ args:
+ - |
+ apt-get update && apt-get install -y git curl gnupg
+
+ # Install NVIDIA drivers and CUDA
+ echo "Installing NVIDIA drivers and CUDA..."
+ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
+ curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
+ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
+ tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
+ apt-get update && apt-get install -y nvidia-container-toolkit
+
+ # Verify GPU is available
+ echo "Verifying GPU availability..."
+ nvidia-smi || echo "WARNING: nvidia-smi command failed. GPU might not be properly configured."
+
+ # Install Ollama
+ echo "Installing Ollama..."
+ curl -fsSL https://ollama.com/install.sh | sh
+
+ # Configure Ollama to use GPU
+ echo "Configuring Ollama for GPU usage..."
+ mkdir -p /root/.ollama
+ echo '{"gpu": {"enable": true}}' > /root/.ollama/config.json
+
+ # Start Ollama in the background with GPU support
+ echo "Starting Ollama service with GPU support..."
+ ollama serve &
+
+ # Wait for Ollama to be ready
+ echo "Waiting for Ollama to be ready..."
+ until curl -s http://localhost:11434/api/tags >/dev/null; do
+ sleep 5
+ done
+
+ # Verify models are using GPU
+ echo "Verifying models are using GPU..."
+ curl -s http://localhost:11434/api/tags | grep -q "llama3" && echo "llama3 model is available"
+
+ # Clone and set up the application
+ cd /app
+ git clone https://github.com/oracle-devrel/devrel-labs.git
+ cd devrel-labs/agentic_rag
+ pip install -r requirements.txt
+
+ # Start the Gradio app
+ echo "Starting Gradio application..."
+ python gradio_app.py
+ env:
+ - name: PYTHONUNBUFFERED
+ value: "1"
+ - name: OLLAMA_HOST
+ value: "http://localhost:11434"
+ - name: NVIDIA_VISIBLE_DEVICES
+ value: "all"
+ - name: NVIDIA_DRIVER_CAPABILITIES
+ value: "compute,utility"
+ - name: TORCH_CUDA_ARCH_LIST
+ value: "7.0;7.5;8.0;8.6"
+ volumes:
+ - name: config-volume
+ configMap:
+ name: agentic-rag-config
+ - name: data-volume
+ persistentVolumeClaim:
+ claimName: agentic-rag-data-pvc
+ - name: chroma-volume
+ persistentVolumeClaim:
+ claimName: agentic-rag-chroma-pvc
+ - name: ollama-models
+ persistentVolumeClaim:
+ claimName: ollama-models-pvc
\ No newline at end of file
diff --git a/financial/ai-agents/k8s/local-deployment/pvcs.yaml b/financial/ai-agents/k8s/local-deployment/pvcs.yaml
new file mode 100644
index 00000000..6ab8ff37
--- /dev/null
+++ b/financial/ai-agents/k8s/local-deployment/pvcs.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: agentic-rag-data-pvc
+ namespace: agentic-rag
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50Gi
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: agentic-rag-chroma-pvc
+ namespace: agentic-rag
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50Gi
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: ollama-models-pvc
+ namespace: agentic-rag
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50Gi # Larger storage for model files
\ No newline at end of file
diff --git a/financial/ai-agents/k8s/local-deployment/service.yaml b/financial/ai-agents/k8s/local-deployment/service.yaml
new file mode 100644
index 00000000..59f67728
--- /dev/null
+++ b/financial/ai-agents/k8s/local-deployment/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: agentic-rag
+ labels:
+ app: agentic-rag
+spec:
+ type: LoadBalancer # Use NodePort if LoadBalancer is not available
+ ports:
+ - port: 80
+ targetPort: 7860
+ protocol: TCP
+ name: http
+ selector:
+ app: agentic-rag
\ No newline at end of file
diff --git a/financial/ai-agents/local_rag_agent.py b/financial/ai-agents/local_rag_agent.py
new file mode 100644
index 00000000..c26f99ed
--- /dev/null
+++ b/financial/ai-agents/local_rag_agent.py
@@ -0,0 +1,613 @@
+from typing import List, Dict, Any, Optional
+from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
+import torch
+from store import VectorStore
+from agents.agent_factory import create_agents
+import argparse
+import yaml
+import os
+import logging
+import time
+import json
+from pathlib import Path
+try:
+ from OraDBVectorStore import OraDBVectorStore
+ ORACLE_DB_AVAILABLE = True
+except ImportError:
+ ORACLE_DB_AVAILABLE = False
+ print("Oracle DB support not available. Install with: pip install oracledb sentence-transformers")
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s',
+ datefmt='%H:%M:%S'
+)
+logger = logging.getLogger(__name__)
+
+class LocalLLM:
+ """Wrapper for local LLM to match LangChain's ChatOpenAI interface"""
+ def __init__(self, pipeline):
+ self.pipeline = pipeline
+
+ def invoke(self, messages):
+ # Convert messages to a single prompt
+ prompt = "\n".join([msg.content for msg in messages])
+ result = self.pipeline(
+ prompt,
+ max_new_tokens=512,
+ do_sample=True,
+ temperature=0.1,
+ top_p=0.95,
+ return_full_text=False
+ )[0]["generated_text"]
+
+ # Create a response object with content attribute
+ class Response:
+ def __init__(self, content):
+ self.content = content
+
+ return Response(result.strip())
+
+class OllamaModelHandler:
+ """Handler for Ollama models"""
+ def __init__(self, model_name: str):
+ """Initialize Ollama model handler
+
+ Args:
+ model_name: Name of the Ollama model to use
+ """
+ # Remove the 'ollama:' prefix if present
+ self.model_name = model_name.replace("ollama:", "") if model_name.startswith("ollama:") else model_name
+ self._check_ollama_running()
+
+ def _check_ollama_running(self):
+ """Check if Ollama is running and the model is available"""
+ try:
+ import ollama
+
+ # Check if Ollama is running
+ try:
+ models = ollama.list().models
+ available_models = [model.model for model in models]
+ print(f"Available Ollama models: {', '.join(available_models)}")
+
+ # Check if the requested model is available
+ if self.model_name not in available_models:
+ # Try with :latest suffix
+ if f"{self.model_name}:latest" in available_models:
+ self.model_name = f"{self.model_name}:latest"
+ print(f"Using model with :latest suffix: {self.model_name}")
+ else:
+ print(f"Model '{self.model_name}' not found in Ollama. Available models: {', '.join(available_models)}")
+ print(f"You can pull it with: ollama pull {self.model_name}")
+ except Exception as e:
+ raise ConnectionError(f"Failed to connect to Ollama. Please make sure Ollama is running. Error: {str(e)}")
+
+ except ImportError:
+ raise ImportError("Failed to import ollama. Please install with: pip install ollama")
+
+ def __call__(self, prompt, max_new_tokens=512, temperature=0.1, top_p=0.95, **kwargs):
+ """Generate text using the Ollama model"""
+ try:
+ import ollama
+
+ # Generate text
+ response = ollama.generate(
+ model=self.model_name,
+ prompt=prompt,
+ options={
+ "num_predict": max_new_tokens,
+ "temperature": temperature,
+ "top_p": top_p
+ }
+ )
+
+ # Format result to match transformers pipeline output
+ formatted_result = [{
+ "generated_text": response["response"]
+ }]
+
+ return formatted_result
+
+ except Exception as e:
+ raise Exception(f"Failed to generate text with Ollama: {str(e)}")
+
+class LocalRAGAgent:
+ def __init__(self, vector_store: VectorStore = None, model_name: str = "mistralai/Mistral-7B-Instruct-v0.2",
+ use_cot: bool = False, collection: str = None, skip_analysis: bool = False,
+ quantization: str = None, use_oracle_db: bool = True):
+ """Initialize local RAG agent with vector store and local LLM
+
+ Args:
+ vector_store: Vector store for retrieving context (if None, will create one)
+ model_name: HuggingFace model name/path or Ollama model name
+ use_cot: Whether to use Chain of Thought reasoning
+ collection: Collection to search in (PDF, Repository, or General Knowledge)
+ skip_analysis: Whether to skip query analysis (kept for backward compatibility)
+ quantization: Quantization method to use (None, '4bit', '8bit')
+ use_oracle_db: Whether to use Oracle DB for vector storage (if False, uses ChromaDB)
+ """
+ # Initialize vector store if not provided
+ self.use_oracle_db = use_oracle_db and ORACLE_DB_AVAILABLE
+
+ if vector_store is None:
+ if self.use_oracle_db:
+ try:
+ self.vector_store = OraDBVectorStore()
+ print("Using Oracle DB for vector storage")
+ except ValueError as ve:
+ if "credentials not found" in str(ve):
+ print(f"Oracle DB credentials not found in config.yaml: {str(ve)}")
+ print("Falling back to ChromaDB")
+ else:
+ print(f"Oracle DB initialization error: {str(ve)}")
+ print("Falling back to ChromaDB")
+ self.vector_store = VectorStore(persist_directory="embeddings")
+ self.use_oracle_db = False
+ except Exception as e:
+ print(f"Error initializing Oracle DB: {str(e)}")
+ print("Falling back to ChromaDB")
+ self.vector_store = VectorStore(persist_directory="embeddings")
+ self.use_oracle_db = False
+ else:
+ self.vector_store = VectorStore(persist_directory="embeddings")
+ print("Using ChromaDB for vector storage")
+ else:
+ self.vector_store = vector_store
+ # Determine type of vector store
+ self.use_oracle_db = hasattr(vector_store, 'connection') and hasattr(vector_store, 'cursor')
+
+ self.use_cot = use_cot
+ self.collection = collection
+ self.quantization = quantization
+ self.model_name = model_name
+ # skip_analysis parameter kept for backward compatibility but no longer used
+
+ # Check if this is an Ollama model
+ self.is_ollama = model_name.startswith("ollama:")
+
+ if self.is_ollama:
+ # Extract the actual model name from the prefix
+ ollama_model_name = model_name.replace("ollama:", "")
+
+ # Load Ollama model
+ print("\nLoading Ollama model...")
+ print(f"Model: {ollama_model_name}")
+ print("Note: Make sure Ollama is running on your system.")
+
+ # Initialize Ollama model handler
+ self.ollama_handler = OllamaModelHandler(ollama_model_name)
+
+ # Create pipeline-like interface
+ self.pipeline = self.ollama_handler
+
+ else:
+ # Load HuggingFace token from config
+ try:
+ with open('config.yaml', 'r') as f:
+ config = yaml.safe_load(f)
+ token = config.get('HUGGING_FACE_HUB_TOKEN')
+ if not token:
+ raise ValueError("HUGGING_FACE_HUB_TOKEN not found in config.yaml")
+ except Exception as e:
+ raise Exception(f"Failed to load HuggingFace token from config.yaml: {str(e)}")
+
+ # Load model and tokenizer
+ print("\nLoading model and tokenizer...")
+ print(f"Model: {model_name}")
+ if quantization:
+ print(f"Quantization: {quantization}")
+ print("Note: Initial loading and inference can take 1-5 minutes depending on your hardware.")
+ print("Subsequent queries will be faster but may still take 30-60 seconds per response.")
+
+ # Check if CUDA is available and set appropriate dtype
+ if torch.cuda.is_available():
+ print("CUDA is available. Using GPU acceleration.")
+ dtype = torch.float16
+ else:
+ print("CUDA is not available. Using CPU only (this will be slow).")
+ dtype = torch.float32
+
+ # Set up model loading parameters
+ model_kwargs = {
+ "torch_dtype": dtype,
+ "device_map": "auto",
+ "token": token,
+ "low_cpu_mem_usage": True,
+ "offload_folder": "offload"
+ }
+
+ # Apply quantization if specified
+ if quantization == '4bit':
+ try:
+ from transformers import BitsAndBytesConfig
+ quantization_config = BitsAndBytesConfig(
+ load_in_4bit=True,
+ bnb_4bit_compute_dtype=torch.float16,
+ bnb_4bit_use_double_quant=True,
+ bnb_4bit_quant_type="nf4"
+ )
+ model_kwargs["quantization_config"] = quantization_config
+ print("Using 4-bit quantization with bitsandbytes")
+ except ImportError:
+ print("Warning: bitsandbytes not installed. Falling back to standard loading.")
+ print("To use 4-bit quantization, install bitsandbytes: pip install bitsandbytes")
+ elif quantization == '8bit':
+ try:
+ from transformers import BitsAndBytesConfig
+ quantization_config = BitsAndBytesConfig(load_in_8bit=True)
+ model_kwargs["quantization_config"] = quantization_config
+ print("Using 8-bit quantization with bitsandbytes")
+ except ImportError:
+ print("Warning: bitsandbytes not installed. Falling back to standard loading.")
+ print("To use 8-bit quantization, install bitsandbytes: pip install bitsandbytes")
+
+ # Load model with appropriate settings
+ self.model = AutoModelForCausalLM.from_pretrained(
+ model_name,
+ **model_kwargs
+ )
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name, token=token)
+
+ # Create text generation pipeline with optimized settings
+ self.pipeline = pipeline(
+ "text-generation",
+ model=self.model,
+ tokenizer=self.tokenizer,
+ max_new_tokens=512,
+ do_sample=True,
+ temperature=0.1,
+ top_p=0.95,
+ device_map="auto"
+ )
+ print("✓ Model loaded successfully")
+
+ # Create LLM wrapper
+ self.llm = LocalLLM(self.pipeline)
+
+ # Initialize specialized agents if CoT is enabled
+ self.agents = create_agents(self.llm, self.vector_store) if use_cot else None
+
+ def process_query(self, query: str) -> Dict[str, Any]:
+ """Process a user query using the agentic RAG pipeline"""
+ logger.info(f"Processing query with collection: {self.collection}")
+
+ # Process based on collection type and CoT setting
+ if self.collection == "General Knowledge":
+ # For General Knowledge, directly use general response
+ if self.use_cot:
+ return self._process_query_with_cot(query)
+ else:
+ return self._generate_general_response(query)
+ else:
+ # For PDF, Repository, or Web collections, use context-based processing
+ if self.use_cot:
+ return self._process_query_with_cot(query)
+ else:
+ return self._process_query_standard(query)
+
+ def _process_query_with_cot(self, query: str) -> Dict[str, Any]:
+ """Process query using Chain of Thought reasoning"""
+ try:
+ # Get context based on collection type
+ if self.collection == "PDF Collection":
+ db_type = "Oracle DB" if self.use_oracle_db else "ChromaDB"
+ print(f"🔄 Using {db_type} for retrieving PDF Collection context")
+ context = self.vector_store.query_pdf_collection(query)
+ elif self.collection == "Repository Collection":
+ db_type = "Oracle DB" if self.use_oracle_db else "ChromaDB"
+ print(f"🔄 Using {db_type} for retrieving Repository Collection context")
+ context = self.vector_store.query_repo_collection(query)
+ elif self.collection == "Web Knowledge Base":
+ db_type = "Oracle DB" if self.use_oracle_db else "ChromaDB"
+ print(f"🔄 Using {db_type} for retrieving Web Knowledge Base context")
+ context = self.vector_store.query_web_collection(query)
+ else:
+ context = []
+
+ # Log number of chunks retrieved
+ logger.info(f"Retrieved {len(context)} chunks from {self.collection}")
+
+ # Create agents if not already created
+ if not self.agents:
+ self.agents = create_agents(self.llm, self.vector_store)
+
+ # Get planning step
+ try:
+ planning_result = self.agents["planner"].plan(query, context)
+ logger.info("Planning step completed")
+ except Exception as e:
+ logger.error(f"Error in planning step: {str(e)}")
+ logger.info("Falling back to general response")
+ return self._generate_general_response(query)
+
+ # Get research step
+ research_results = []
+ if self.agents.get("researcher") is not None and context:
+ for step in planning_result.split("\n"):
+ if not step.strip():
+ continue
+ try:
+ step_research = self.agents["researcher"].research(query, step)
+ # Extract findings from research result
+ findings = step_research.get("findings", []) if isinstance(step_research, dict) else []
+ research_results.append({"step": step, "findings": findings})
+
+ # Log which sources were used for this step
+ try:
+ source_indices = [context.index(finding) + 1 for finding in findings if finding in context]
+ logger.info(f"Research for step: {step}\nUsing sources: {source_indices}")
+ except ValueError as ve:
+ logger.warning(f"Could not find some findings in initial context: {str(ve)}")
+ except Exception as e:
+ logger.error(f"Error during research for step '{step}': {str(e)}")
+ research_results.append({"step": step, "findings": []})
+ else:
+ # If no researcher or no context, use the steps directly
+ research_results = [{"step": step, "findings": []} for step in planning_result.split("\n") if step.strip()]
+ logger.info("No research performed (no researcher agent or no context available)")
+
+ # Get reasoning step
+ reasoning_steps = []
+ if not self.agents.get("reasoner"):
+ logger.warning("No reasoner agent available, using direct response")
+ return self._generate_general_response(query)
+
+ for result in research_results:
+ try:
+ step_reasoning = self.agents["reasoner"].reason(
+ query,
+ result["step"],
+ result["findings"] if result["findings"] else [{"content": "Using general knowledge", "metadata": {"source": "General Knowledge"}}]
+ )
+ reasoning_steps.append(step_reasoning)
+ logger.info(f"Reasoning for step: {result['step']}\n{step_reasoning}")
+ except Exception as e:
+ logger.error(f"Error in reasoning for step '{result['step']}': {str(e)}")
+ reasoning_steps.append(f"Error in reasoning for this step: {str(e)}")
+
+ # Get synthesis step
+ if not self.agents.get("synthesizer"):
+ logger.warning("No synthesizer agent available, using direct response")
+ return self._generate_general_response(query)
+
+ try:
+ synthesis_result = self.agents["synthesizer"].synthesize(query, reasoning_steps)
+ logger.info("Synthesis step completed")
+ except Exception as e:
+ logger.error(f"Error in synthesis step: {str(e)}")
+ logger.info("Falling back to general response")
+ return self._generate_general_response(query)
+
+ return {
+ "answer": synthesis_result["answer"],
+ "reasoning_steps": reasoning_steps,
+ "context": context
+ }
+
+ except Exception as e:
+ logger.error(f"Error in CoT processing: {str(e)}")
+ raise
+
+ def _process_query_standard(self, query: str) -> Dict[str, Any]:
+ """Process query using standard RAG approach"""
+ try:
+ # Get context based on collection type
+ if self.collection == "PDF Collection":
+ db_type = "Oracle DB" if self.use_oracle_db else "ChromaDB"
+ print(f"🔄 Using {db_type} for retrieving PDF Collection context")
+ context = self.vector_store.query_pdf_collection(query)
+ elif self.collection == "Repository Collection":
+ db_type = "Oracle DB" if self.use_oracle_db else "ChromaDB"
+ print(f"🔄 Using {db_type} for retrieving Repository Collection context")
+ context = self.vector_store.query_repo_collection(query)
+ elif self.collection == "Web Knowledge Base":
+ db_type = "Oracle DB" if self.use_oracle_db else "ChromaDB"
+ print(f"🔄 Using {db_type} for retrieving Web Knowledge Base context")
+ context = self.vector_store.query_web_collection(query)
+ else:
+ context = []
+
+ # Log number of chunks retrieved
+ logger.info(f"Retrieved {len(context)} chunks from {self.collection}")
+
+ # Generate response using context
+ response = self._generate_response(query, context)
+ return response
+
+ except Exception as e:
+ logger.error(f"Error in standard processing: {str(e)}")
+ raise
+
+ def _generate_text(self, prompt: str, max_length: int = 512) -> str:
+ """Generate text using the local model"""
+ # Log start time for performance monitoring
+ start_time = time.time()
+
+ result = self.pipeline(
+ prompt,
+ max_new_tokens=max_length,
+ do_sample=True,
+ temperature=0.1,
+ top_p=0.95,
+ return_full_text=False
+ )[0]["generated_text"]
+
+ # Log completion time
+ elapsed_time = time.time() - start_time
+ logger.info(f"Text generation completed in {elapsed_time:.2f} seconds")
+
+ return result.strip()
+
+ def _generate_response(self, query: str, context: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """Generate a response using the retrieved context"""
+ context_str = "\n\n".join([f"Context {i+1}:\n{item['content']}"
+ for i, item in enumerate(context)])
+
+ template = """Answer the following query using the provided context.
+Respond as if you are knowledgeable about the topic and incorporate the context naturally.
+Do not mention limitations in the context or that you couldn't find specific information.
+
+Context:
+{context}
+
+Query: {query}
+
+Answer:"""
+
+ prompt = template.format(context=context_str, query=query)
+ response_text = self._generate_text(prompt)
+
+ # Add sources to response if available
+ sources = {}
+ if context:
+ # Group sources by document
+ for item in context:
+ # Handle metadata which could be a string (from Oracle DB) or a dict (from ChromaDB)
+ metadata = item['metadata']
+ if isinstance(metadata, str):
+ try:
+ metadata = json.loads(metadata)
+ except json.JSONDecodeError:
+ metadata = {"source": "Unknown"}
+
+ source = metadata.get('source', 'Unknown')
+ if source not in sources:
+ sources[source] = set()
+
+ # Add page number if available
+ if 'page' in metadata:
+ sources[source].add(str(metadata['page']))
+ # Add file path if available for code
+ if 'file_path' in metadata:
+ sources[source] = metadata['file_path']
+
+ # Print concise source information
+ print("\nSources detected:")
+ # Print a single line for each source without additional details
+ for source in sources:
+ print(f"- {source}")
+
+ return {
+ "answer": response_text,
+ "context": context,
+ "sources": sources
+ }
+
+ def _generate_general_response(self, query: str) -> Dict[str, Any]:
+ """Generate a response using general knowledge when no context is available"""
+ template = """You are a helpful AI assistant. Answer the following query using your general knowledge.
+
+Query: {query}
+
+Answer:"""
+
+ prompt = template.format(query=query)
+ response = self._generate_text(prompt)
+
+ return {
+ "answer": response,
+ "context": []
+ }
+
+def main():
+ parser = argparse.ArgumentParser(description="Query documents using local LLM")
+ parser.add_argument("--query", required=True, help="Query to search for")
+ parser.add_argument("--embeddings", default="oracle", choices=["oracle", "chromadb"], help="Embeddings backend to use")
+ parser.add_argument("--model", default="ollama:qwen2", help="Model to use (default: ollama:qwen2)")
+ parser.add_argument("--collection", help="Collection to search (PDF, Repository, General Knowledge)")
+ parser.add_argument("--use-cot", action="store_true", help="Use Chain of Thought reasoning")
+ parser.add_argument("--store-path", default="embeddings", help="Path to ChromaDB store")
+ parser.add_argument("--skip-analysis", action="store_true", help="Skip query analysis (not recommended)")
+ parser.add_argument("--verbose", action="store_true", help="Show full content of sources")
+ parser.add_argument("--quiet", action="store_true", help="Disable verbose logging")
+ parser.add_argument("--quantization", choices=["4bit", "8bit"], help="Quantization method (4bit or 8bit)")
+
+ args = parser.parse_args()
+
+ # Set logging level based on quiet flag
+ if args.quiet:
+ logger.setLevel(logging.WARNING)
+ else:
+ logger.setLevel(logging.INFO)
+
+ print("\nInitializing RAG agent...")
+ print("=" * 50)
+
+ try:
+ # Determine which vector store to use based on args.embeddings
+ if args.embeddings == "oracle" and ORACLE_DB_AVAILABLE:
+ try:
+ logger.info("Initializing Oracle DB vector store")
+ store = OraDBVectorStore()
+ print("✓ Using Oracle DB for vector storage")
+ except Exception as e:
+ logger.warning(f"Failed to initialize Oracle DB: {str(e)}")
+ logger.info(f"Falling back to ChromaDB from: {args.store_path}")
+ store = VectorStore(persist_directory=args.store_path)
+ print("⚠ Oracle DB initialization failed, using ChromaDB instead")
+ else:
+ if args.embeddings == "oracle" and not ORACLE_DB_AVAILABLE:
+ logger.warning("Oracle DB support not available")
+ print("⚠ Oracle DB support not available (missing dependencies)")
+
+ logger.info(f"Initializing ChromaDB vector store from: {args.store_path}")
+ store = VectorStore(persist_directory=args.store_path)
+ print("✓ Using ChromaDB for vector storage")
+
+ logger.info("Initializing local RAG agent...")
+ # Set use_oracle_db based on the actual store type
+ use_oracle_db = args.embeddings == "oracle" and isinstance(store, OraDBVectorStore)
+
+ agent = LocalRAGAgent(
+ store,
+ model_name=args.model,
+ use_cot=args.use_cot,
+ collection=args.collection,
+ skip_analysis=args.skip_analysis,
+ use_oracle_db=use_oracle_db
+ )
+
+ print(f"\nProcessing query: {args.query}")
+ print("=" * 50)
+
+ response = agent.process_query(args.query)
+
+ print("\nResponse:")
+ print("-" * 50)
+ print(response["answer"])
+
+ if response.get("reasoning_steps"):
+ print("\nReasoning Steps:")
+ print("-" * 50)
+ for i, step in enumerate(response["reasoning_steps"]):
+ print(f"\nStep {i+1}:")
+ print(step)
+
+ if response.get("context"):
+ print("\nSources used:")
+ print("-" * 50)
+
+ # Print concise list of sources
+ for i, ctx in enumerate(response["context"]):
+ source = ctx["metadata"].get("source", "Unknown")
+ if "page_numbers" in ctx["metadata"]:
+ pages = ctx["metadata"].get("page_numbers", [])
+ print(f"[{i+1}] {source} (pages: {pages})")
+ else:
+ file_path = ctx["metadata"].get("file_path", "Unknown")
+ print(f"[{i+1}] {source} (file: {file_path})")
+
+ # Only print content if verbose flag is set
+ if args.verbose:
+ content_preview = ctx["content"][:300] + "..." if len(ctx["content"]) > 300 else ctx["content"]
+ print(f" Content: {content_preview}\n")
+
+ except Exception as e:
+ logger.error(f"Error during execution: {str(e)}", exc_info=True)
+ print(f"\n✗ Error: {str(e)}")
+ exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/main.py b/financial/ai-agents/main.py
new file mode 100644
index 00000000..41b8fa22
--- /dev/null
+++ b/financial/ai-agents/main.py
@@ -0,0 +1,158 @@
+import os
+from typing import List, Optional
+from fastapi import FastAPI, File, UploadFile, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from dotenv import load_dotenv
+import uuid
+
+from pdf_processor import PDFProcessor
+from store import VectorStore
+from local_rag_agent import LocalRAGAgent
+from rag_agent import RAGAgent
+
+# Load environment variables
+load_dotenv()
+
+# Initialize FastAPI app
+app = FastAPI(
+ title="Agentic RAG API",
+ description="API for processing PDFs and answering queries using an Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph)",
+ version="1.0.0"
+)
+
+# Add CORS middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Initialize components
+pdf_processor = PDFProcessor()
+vector_store = VectorStore()
+
+# Check for Ollama availability
+try:
+ import ollama
+ ollama_available = True
+ print("\nOllama is available. You can use Ollama models for RAG.")
+except ImportError:
+ ollama_available = False
+ print("\nOllama not installed. You can install it with: pip install ollama")
+
+# Initialize RAG agent - use OpenAI if API key is available, otherwise use local model or Ollama
+openai_api_key = os.getenv("OPENAI_API_KEY")
+if openai_api_key:
+ print("\nUsing OpenAI GPT-4 for RAG...")
+ rag_agent = RAGAgent(vector_store=vector_store, openai_api_key=openai_api_key)
+else:
+ # Try to use local Mistral model first
+ try:
+ print("\nTrying to use local Mistral model...")
+ rag_agent = LocalRAGAgent(vector_store=vector_store)
+ print("Successfully initialized local Mistral model.")
+ except Exception as e:
+ print(f"\nFailed to initialize local Mistral model: {str(e)}")
+
+ # Fall back to Ollama if Mistral fails and Ollama is available
+ if ollama_available:
+ try:
+ print("\nFalling back to Ollama with llama3 model...")
+ rag_agent = LocalRAGAgent(vector_store=vector_store, model_name="ollama:llama3")
+ print("Successfully initialized Ollama with llama3 model.")
+ except Exception as e:
+ print(f"\nFailed to initialize Ollama: {str(e)}")
+ print("No available models. Please check your configuration.")
+ raise e
+ else:
+ print("\nNo available models. Please check your configuration.")
+ raise e
+
+class QueryRequest(BaseModel):
+ query: str
+ use_cot: bool = False
+ model: Optional[str] = None # Allow specifying model in the request
+
+class QueryResponse(BaseModel):
+ answer: str
+ reasoning: Optional[str] = None
+ context: List[dict]
+
+@app.post("/upload/pdf")
+async def upload_pdf(file: UploadFile = File(...)):
+ """Upload and process a PDF file"""
+ if not file.filename.lower().endswith('.pdf'):
+ raise HTTPException(status_code=400, detail="File must be a PDF")
+
+ try:
+ # Save the uploaded file temporarily
+ temp_path = f"temp_{uuid.uuid4()}.pdf"
+ with open(temp_path, "wb") as buffer:
+ content = await file.read()
+ buffer.write(content)
+
+ # Process the PDF
+ chunks, document_id = pdf_processor.process_pdf(temp_path)
+
+ # Add chunks to vector store
+ vector_store.add_pdf_chunks(chunks, document_id=document_id)
+
+ # Clean up
+ os.remove(temp_path)
+
+ return {
+ "message": "PDF processed successfully",
+ "document_id": document_id,
+ "chunks_processed": len(chunks)
+ }
+
+ except Exception as e:
+ if os.path.exists(temp_path):
+ os.remove(temp_path)
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/query", response_model=QueryResponse)
+async def query(request: QueryRequest):
+ """Process a query using the RAG agent"""
+ try:
+ # Determine which model to use
+ if request.model:
+ if request.model.startswith("ollama:") and ollama_available:
+ # Use specified Ollama model
+ rag_agent = LocalRAGAgent(vector_store=vector_store, model_name=request.model, use_cot=request.use_cot)
+ elif request.model == "openai" and openai_api_key:
+ # Use OpenAI
+ rag_agent = RAGAgent(vector_store=vector_store, openai_api_key=openai_api_key, use_cot=request.use_cot)
+ else:
+ # Use default local model
+ rag_agent = LocalRAGAgent(vector_store=vector_store, use_cot=request.use_cot)
+ else:
+ # Reinitialize agent with CoT setting using default model
+ if openai_api_key:
+ rag_agent = RAGAgent(vector_store=vector_store, openai_api_key=openai_api_key, use_cot=request.use_cot)
+ else:
+ # Try local Mistral first
+ try:
+ rag_agent = LocalRAGAgent(vector_store=vector_store, use_cot=request.use_cot)
+ except Exception as e:
+ print(f"Failed to initialize local Mistral model: {str(e)}")
+ # Fall back to Ollama if available
+ if ollama_available:
+ try:
+ rag_agent = LocalRAGAgent(vector_store=vector_store, model_name="ollama:llama3", use_cot=request.use_cot)
+ except Exception as e2:
+ raise Exception(f"Failed to initialize any model: {str(e2)}")
+ else:
+ raise e
+
+ response = rag_agent.process_query(request.query)
+ return response
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/financial/ai-agents/pdf_processor.py b/financial/ai-agents/pdf_processor.py
new file mode 100644
index 00000000..03fc0ebb
--- /dev/null
+++ b/financial/ai-agents/pdf_processor.py
@@ -0,0 +1,248 @@
+from pathlib import Path
+from typing import List, Dict, Any
+import json
+import argparse
+from docling.document_converter import DocumentConverter
+from docling.chunking import HybridChunker
+from urllib.parse import urlparse
+import warnings
+import transformers
+import uuid # Add at the top with other imports
+
+# Suppress the token length warning
+warnings.filterwarnings('ignore', category=UserWarning, module='transformers.generation.utils')
+
+def is_url(string: str) -> bool:
+ """Check if a string is a valid URL"""
+ try:
+ result = urlparse(string)
+ return all([result.scheme, result.netloc])
+ except:
+ return False
+
+class PDFProcessor:
+ def __init__(self, tokenizer: str = "BAAI/bge-small-en-v1.5"):
+ """Initialize PDF processor with Docling components"""
+ # Suppress CUDA compilation warnings
+ warnings.filterwarnings('ignore', category=UserWarning, module='torch.utils.cpp_extension')
+ # Suppress token length warnings
+ warnings.filterwarnings('ignore', category=UserWarning, module='transformers.generation.utils')
+ warnings.filterwarnings('ignore', category=UserWarning, module='transformers.modeling_utils')
+
+ self.converter = DocumentConverter()
+ self.tokenizer = tokenizer
+
+ def _extract_metadata(self, meta: Any) -> Dict[str, Any]:
+ """Safely extract metadata from various object types"""
+ try:
+ if hasattr(meta, '__dict__'):
+ # If it's an object with attributes
+ return {
+ "headings": getattr(meta, "headings", []),
+ "page_numbers": self._extract_page_numbers(meta)
+ }
+ elif isinstance(meta, dict):
+ # If it's a dictionary
+ return {
+ "headings": meta.get("headings", []),
+ "page_numbers": self._extract_page_numbers(meta)
+ }
+ else:
+ # Default empty metadata
+ return {
+ "headings": [],
+ "page_numbers": []
+ }
+ except Exception as e:
+ print(f"Warning: Error extracting metadata: {str(e)}")
+ return {
+ "headings": [],
+ "page_numbers": []
+ }
+
+ def _try_chunk_with_size(self, document: Any, chunk_size: int) -> List[Any]:
+ """Try chunking with a specific size, return None if it fails"""
+ try:
+ # Create a new chunker with the specified size
+ chunker = HybridChunker(
+ tokenizer=self.tokenizer,
+ chunk_size=chunk_size,
+ chunk_overlap=0.1
+ )
+ return list(chunker.chunk(document))
+ except Exception as e:
+ print(f"Warning: Chunking failed with size {chunk_size}: {str(e)}")
+ return None
+
+ def process_pdf(self, file_path: str | Path) -> List[Dict[str, Any]]:
+ """Process a PDF file and return chunks of text with metadata"""
+ try:
+ # Generate a unique document ID
+ document_id = str(uuid.uuid4())
+
+ # Convert PDF using Docling
+ conv_result = self.converter.convert(file_path)
+ if not conv_result or not conv_result.document:
+ raise ValueError(f"Failed to convert PDF: {file_path}")
+
+ # Try chunking with progressively smaller sizes
+ chunks = None
+ for chunk_size in [200, 150, 100, 75]:
+ chunks = self._try_chunk_with_size(conv_result.document, chunk_size)
+ if chunks:
+ print(f"Successfully chunked with size {chunk_size}")
+ break
+
+ if not chunks:
+ raise ValueError("Failed to chunk document with any chunk size")
+
+ # Process chunks into a standardized format
+ processed_chunks = []
+ for chunk in chunks:
+ # Handle both dictionary and DocChunk objects
+ text = chunk.text if hasattr(chunk, 'text') else chunk.get('text', '')
+ meta = chunk.meta if hasattr(chunk, 'meta') else chunk.get('meta', {})
+
+ metadata = self._extract_metadata(meta)
+ metadata["source"] = str(file_path)
+ metadata["document_id"] = document_id # Add document_id to metadata
+
+ processed_chunk = {
+ "text": text,
+ "metadata": metadata
+ }
+ processed_chunks.append(processed_chunk)
+
+ return processed_chunks, document_id # Return both chunks and document_id
+
+ except Exception as e:
+ raise Exception(f"Error processing PDF {file_path}: {str(e)}")
+
+ def process_pdf_url(self, url: str) -> List[Dict[str, Any]]:
+ """Process a PDF file from a URL and return chunks of text with metadata"""
+ try:
+ # Convert PDF using Docling's built-in URL support
+ conv_result = self.converter.convert(url)
+ if not conv_result or not conv_result.document:
+ raise ValueError(f"Failed to convert PDF from URL: {url}")
+
+ # Generate a unique document ID
+ document_id = str(uuid.uuid4())
+
+ # Chunk the document
+ chunks = list(self.chunker.chunk(conv_result.document))
+
+ # Process chunks into a standardized format
+ processed_chunks = []
+ for chunk in chunks:
+ # Handle both dictionary and DocChunk objects
+ text = chunk.text if hasattr(chunk, 'text') else chunk.get('text', '')
+ meta = chunk.meta if hasattr(chunk, 'meta') else chunk.get('meta', {})
+
+ metadata = self._extract_metadata(meta)
+ metadata["source"] = url
+ metadata["document_id"] = document_id
+
+ processed_chunk = {
+ "text": text,
+ "metadata": metadata
+ }
+ processed_chunks.append(processed_chunk)
+
+ return processed_chunks, document_id
+
+ except Exception as e:
+ raise Exception(f"Error processing PDF from URL {url}: {str(e)}")
+
+ def process_directory(self, directory: str | Path) -> List[Dict[str, Any]]:
+ """Process all PDF files in a directory"""
+ directory = Path(directory)
+ all_chunks = []
+ document_ids = []
+
+ for pdf_file in directory.glob("**/*.pdf"):
+ try:
+ chunks, doc_id = self.process_pdf(pdf_file)
+ all_chunks.extend(chunks)
+ document_ids.append(doc_id)
+ print(f"✓ Processed {pdf_file} (ID: {doc_id})")
+ except Exception as e:
+ print(f"✗ Failed to process {pdf_file}: {str(e)}")
+
+ return all_chunks, document_ids
+
+ def _extract_page_numbers(self, meta: Any) -> List[int]:
+ """Extract page numbers from chunk metadata"""
+ page_numbers = set()
+ try:
+ if hasattr(meta, 'doc_items'):
+ items = meta.doc_items
+ elif isinstance(meta, dict) and 'doc_items' in meta:
+ items = meta['doc_items']
+ else:
+ return []
+
+ for item in items:
+ if hasattr(item, 'prov'):
+ provs = item.prov
+ elif isinstance(item, dict) and 'prov' in item:
+ provs = item['prov']
+ else:
+ continue
+
+ for prov in provs:
+ if hasattr(prov, 'page_no'):
+ page_numbers.add(prov.page_no)
+ elif isinstance(prov, dict) and 'page_no' in prov:
+ page_numbers.add(prov['page_no'])
+
+ return sorted(list(page_numbers))
+ except Exception as e:
+ print(f"Warning: Error extracting page numbers: {str(e)}")
+ return []
+
+def main():
+ parser = argparse.ArgumentParser(description="Process PDF files and extract text chunks")
+ parser.add_argument("--input", required=True,
+ help="Input PDF file, directory, or URL (http/https URLs supported)")
+ parser.add_argument("--output", required=True, help="Output JSON file for chunks")
+ parser.add_argument("--tokenizer", default="BAAI/bge-small-en-v1.5", help="Tokenizer to use for chunking")
+
+ args = parser.parse_args()
+ processor = PDFProcessor(tokenizer=args.tokenizer)
+
+ try:
+ # Create output directory if it doesn't exist
+ output_dir = Path(args.output).parent
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ if is_url(args.input):
+ print(f"\nProcessing PDF from URL: {args.input}")
+ print("=" * 50)
+ chunks, doc_id = processor.process_pdf_url(args.input)
+ print(f"Document ID: {doc_id}")
+ elif Path(args.input).is_dir():
+ print(f"\nProcessing directory: {args.input}")
+ print("=" * 50)
+ chunks, doc_ids = processor.process_directory(args.input)
+ print(f"Document IDs: {', '.join(doc_ids)}")
+ else:
+ print(f"\nProcessing file: {args.input}")
+ print("=" * 50)
+ chunks, doc_id = processor.process_pdf(args.input)
+ print(f"Document ID: {doc_id}")
+
+ # Save chunks to JSON
+ with open(args.output, 'w', encoding='utf-8') as f:
+ json.dump(chunks, f, ensure_ascii=False, indent=2)
+
+ print("\nSummary:")
+ print(f"✓ Processed {len(chunks)} chunks")
+ print(f"✓ Saved to {args.output}")
+
+ except Exception as e:
+ print(f"\n✗ Error: {str(e)}")
+ exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/rag_agent.py b/financial/ai-agents/rag_agent.py
new file mode 100644
index 00000000..5d36a54a
--- /dev/null
+++ b/financial/ai-agents/rag_agent.py
@@ -0,0 +1,378 @@
+from typing import List, Dict, Any, Optional
+from langchain_openai import ChatOpenAI
+from langchain.prompts import ChatPromptTemplate
+from store import VectorStore
+from agents.agent_factory import create_agents
+import os
+import argparse
+from dotenv import load_dotenv
+import logging
+
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+class RAGAgent:
+ def __init__(self, vector_store: VectorStore, openai_api_key: str, use_cot: bool = False, collection: str = None, skip_analysis: bool = False):
+ """Initialize RAG agent with vector store and LLM"""
+ self.vector_store = vector_store
+ self.use_cot = use_cot
+ self.collection = collection
+ # skip_analysis parameter kept for backward compatibility but no longer used
+ self.llm = ChatOpenAI(
+ model="gpt-4-turbo-preview",
+ temperature=0,
+ api_key=openai_api_key
+ )
+
+ # Initialize specialized agents
+ self.agents = create_agents(self.llm, vector_store) if use_cot else None
+
+ def process_query(self, query: str) -> Dict[str, Any]:
+ """Process a user query using the agentic RAG pipeline"""
+ logger.info(f"Processing query with collection: {self.collection}")
+
+ # Process based on collection type and CoT setting
+ if self.collection == "General Knowledge":
+ # For General Knowledge, directly use general response
+ if self.use_cot:
+ return self._process_query_with_cot(query)
+ else:
+ return self._generate_general_response(query)
+ else:
+ # For PDF or Repository collections, use context-based processing
+ if self.use_cot:
+ return self._process_query_with_cot(query)
+ else:
+ return self._process_query_standard(query)
+
+ def _process_query_with_cot(self, query: str) -> Dict[str, Any]:
+ """Process query using Chain of Thought reasoning with multiple agents"""
+ logger.info("Processing query with Chain of Thought reasoning")
+
+ # Get initial context based on selected collection
+ initial_context = []
+ if self.collection == "PDF Collection":
+ logger.info(f"Retrieving context from PDF Collection for query: '{query}'")
+ pdf_context = self.vector_store.query_pdf_collection(query)
+ initial_context.extend(pdf_context)
+ logger.info(f"Retrieved {len(pdf_context)} chunks from PDF Collection")
+ # Log each chunk with citation number but not full content
+ for i, chunk in enumerate(pdf_context):
+ source = chunk["metadata"].get("source", "Unknown")
+ pages = chunk["metadata"].get("page_numbers", [])
+ logger.info(f"Source [{i+1}]: {source} (pages: {pages})")
+ # Only log content preview at debug level
+ content_preview = chunk["content"][:150] + "..." if len(chunk["content"]) > 150 else chunk["content"]
+ logger.debug(f"Content preview for source [{i+1}]: {content_preview}")
+ elif self.collection == "Repository Collection":
+ logger.info(f"Retrieving context from Repository Collection for query: '{query}'")
+ repo_context = self.vector_store.query_repo_collection(query)
+ initial_context.extend(repo_context)
+ logger.info(f"Retrieved {len(repo_context)} chunks from Repository Collection")
+ # Log each chunk with citation number but not full content
+ for i, chunk in enumerate(repo_context):
+ source = chunk["metadata"].get("source", "Unknown")
+ file_path = chunk["metadata"].get("file_path", "Unknown")
+ logger.info(f"Source [{i+1}]: {source} (file: {file_path})")
+ # Only log content preview at debug level
+ content_preview = chunk["content"][:150] + "..." if len(chunk["content"]) > 150 else chunk["content"]
+ logger.debug(f"Content preview for source [{i+1}]: {content_preview}")
+ elif self.collection == "Web Knowledge Base":
+ logger.info(f"Retrieving context from Web Knowledge Base for query: '{query}'")
+ web_context = self.vector_store.query_web_collection(query)
+ initial_context.extend(web_context)
+ logger.info(f"Retrieved {len(web_context)} chunks from Web Knowledge Base")
+ # Log each chunk with citation number but not full content
+ for i, chunk in enumerate(web_context):
+ source = chunk["metadata"].get("source", "Unknown")
+ title = chunk["metadata"].get("title", "Unknown")
+ logger.info(f"Source [{i+1}]: {source} (title: {title})")
+ # Only log content preview at debug level
+ content_preview = chunk["content"][:150] + "..." if len(chunk["content"]) > 150 else chunk["content"]
+ logger.debug(f"Content preview for source [{i+1}]: {content_preview}")
+ # For General Knowledge, no context is needed
+ else:
+ logger.info("Using General Knowledge collection, no context retrieval needed")
+
+ try:
+ # Step 1: Planning
+ logger.info("Step 1: Planning")
+ if not self.agents or "planner" not in self.agents:
+ logger.warning("No planner agent available, using direct response")
+ return self._generate_general_response(query)
+
+ try:
+ plan = self.agents["planner"].plan(query, initial_context)
+ logger.info(f"Generated plan:\n{plan}")
+ except Exception as e:
+ logger.error(f"Error in planning step: {str(e)}")
+ logger.info("Falling back to general response")
+ return self._generate_general_response(query)
+
+ # Step 2: Research each step (if researcher is available)
+ logger.info("Step 2: Research")
+ research_results = []
+ if self.agents.get("researcher") is not None and initial_context:
+ for step in plan.split("\n"):
+ if not step.strip():
+ continue
+ try:
+ step_research = self.agents["researcher"].research(query, step)
+ # Extract findings from research result
+ findings = step_research.get("findings", []) if isinstance(step_research, dict) else []
+ research_results.append({"step": step, "findings": findings})
+
+ # Log which sources were used for this step
+ try:
+ source_indices = [initial_context.index(finding) + 1 for finding in findings if finding in initial_context]
+ logger.info(f"Research for step: {step}\nUsing sources: {source_indices}")
+ except ValueError as ve:
+ logger.warning(f"Could not find some findings in initial context: {str(ve)}")
+ except Exception as e:
+ logger.error(f"Error during research for step '{step}': {str(e)}")
+ research_results.append({"step": step, "findings": []})
+ else:
+ # If no researcher or no context, use the steps directly
+ research_results = [{"step": step, "findings": []} for step in plan.split("\n") if step.strip()]
+ logger.info("No research performed (no researcher agent or no context available)")
+
+ # Step 3: Reasoning about each step
+ logger.info("Step 3: Reasoning")
+ if not self.agents.get("reasoner"):
+ logger.warning("No reasoner agent available, using direct response")
+ return self._generate_general_response(query)
+
+ reasoning_steps = []
+ for result in research_results:
+ try:
+ step_reasoning = self.agents["reasoner"].reason(
+ query,
+ result["step"],
+ result["findings"] if result["findings"] else [{"content": "Using general knowledge", "metadata": {"source": "General Knowledge"}}]
+ )
+ reasoning_steps.append(step_reasoning)
+ logger.info(f"Reasoning for step: {result['step']}\n{step_reasoning}")
+ except Exception as e:
+ logger.error(f"Error in reasoning for step '{result['step']}': {str(e)}")
+ reasoning_steps.append(f"Error in reasoning for this step: {str(e)}")
+
+ # Step 4: Synthesize final answer
+ logger.info("Step 4: Synthesis")
+ if not self.agents.get("synthesizer"):
+ logger.warning("No synthesizer agent available, using direct response")
+ return self._generate_general_response(query)
+
+ try:
+ final_answer = self.agents["synthesizer"].synthesize(query, reasoning_steps)
+ logger.info(f"Final synthesized answer:\n{final_answer}")
+ except Exception as e:
+ logger.error(f"Error in synthesis step: {str(e)}")
+ logger.info("Falling back to general response")
+ return self._generate_general_response(query)
+
+ return {
+ "answer": final_answer,
+ "context": initial_context,
+ "reasoning_steps": reasoning_steps
+ }
+ except Exception as e:
+ logger.error(f"Error in CoT processing: {str(e)}", exc_info=True)
+ logger.info("Falling back to general response")
+ return self._generate_general_response(query)
+
+ def _process_query_standard(self, query: str) -> Dict[str, Any]:
+ """Process query using standard approach without Chain of Thought"""
+ # Initialize context variables
+ context = []
+
+ # Get context based on selected collection
+ if self.collection == "PDF Collection":
+ logger.info(f"Retrieving context from PDF Collection for query: '{query}'")
+ context = self.vector_store.query_pdf_collection(query)
+ logger.info(f"Retrieved {len(context)} chunks from PDF Collection")
+ # Log each chunk with citation number but not full content
+ for i, chunk in enumerate(context):
+ source = chunk["metadata"].get("source", "Unknown")
+ pages = chunk["metadata"].get("page_numbers", [])
+ logger.info(f"Source [{i+1}]: {source} (pages: {pages})")
+ # Only log content preview at debug level
+ content_preview = chunk["content"][:150] + "..." if len(chunk["content"]) > 150 else chunk["content"]
+ logger.debug(f"Content preview for source [{i+1}]: {content_preview}")
+ elif self.collection == "Repository Collection":
+ logger.info(f"Retrieving context from Repository Collection for query: '{query}'")
+ context = self.vector_store.query_repo_collection(query)
+ logger.info(f"Retrieved {len(context)} chunks from Repository Collection")
+ # Log each chunk with citation number but not full content
+ for i, chunk in enumerate(context):
+ source = chunk["metadata"].get("source", "Unknown")
+ file_path = chunk["metadata"].get("file_path", "Unknown")
+ logger.info(f"Source [{i+1}]: {source} (file: {file_path})")
+ # Only log content preview at debug level
+ content_preview = chunk["content"][:150] + "..." if len(chunk["content"]) > 150 else chunk["content"]
+ logger.debug(f"Content preview for source [{i+1}]: {content_preview}")
+ elif self.collection == "Web Knowledge Base":
+ logger.info(f"Retrieving context from Web Knowledge Base for query: '{query}'")
+ context = self.vector_store.query_web_collection(query)
+ logger.info(f"Retrieved {len(context)} chunks from Web Knowledge Base")
+ # Log each chunk with citation number but not full content
+ for i, chunk in enumerate(context):
+ source = chunk["metadata"].get("source", "Unknown")
+ title = chunk["metadata"].get("title", "Unknown")
+ logger.info(f"Source [{i+1}]: {source} (title: {title})")
+ # Only log content preview at debug level
+ content_preview = chunk["content"][:150] + "..." if len(chunk["content"]) > 150 else chunk["content"]
+ logger.debug(f"Content preview for source [{i+1}]: {content_preview}")
+
+ # Generate response using context if available, otherwise use general knowledge
+ if context:
+ logger.info(f"Generating response using {len(context)} context chunks")
+ response = self._generate_response(query, context)
+ else:
+ logger.info("No context found, using general knowledge")
+ response = self._generate_general_response(query)
+
+ return response
+
+ def _generate_response(self, query: str, context: List[Dict[str, Any]]) -> Dict[str, Any]:
+ """Generate a response based on the query and context"""
+ # Format context for the prompt
+ formatted_context = "\n\n".join([f"Context {i+1}:\n{item['content']}"
+ for i, item in enumerate(context)])
+
+ # Create the prompt
+ system_prompt = """You are an AI assistant answering questions based on the provided context.
+Answer the question based on the context provided. If the answer is not in the context, say "I don't have enough information to answer this question." Be concise and accurate."""
+
+ # Create messages for the chat model
+ messages = [
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": f"Context:\n{formatted_context}\n\nQuestion: {query}"}
+ ]
+
+ # Generate response
+ response = self.llm.invoke(messages)
+
+ # Add sources to response if available
+ if context:
+ # Group sources by document
+ sources = {}
+ for item in context:
+ source = item['metadata'].get('source', 'Unknown')
+ if source not in sources:
+ sources[source] = set()
+
+ # Add page number if available
+ if 'page' in item['metadata']:
+ sources[source].add(str(item['metadata']['page']))
+ # Add file path if available for code
+ if 'file_path' in item['metadata']:
+ sources[source] = item['metadata']['file_path']
+
+ # Print concise source information
+ print("\nSources detected:")
+ for source, details in sources.items():
+ if isinstance(details, set): # PDF with pages
+ pages = ", ".join(sorted(details))
+ print(f"Document: {source} (pages: {pages})")
+ else: # Code with file path
+ print(f"Code file: {source}")
+
+ response['sources'] = sources
+
+ return {
+ "answer": response.content,
+ "context": context
+ }
+
+ def _generate_general_response(self, query: str) -> Dict[str, Any]:
+ """Generate a response using general knowledge when no context is available"""
+ template = """You are a helpful AI assistant. Answer the following query using your general knowledge.
+
+Query: {query}
+
+Answer:"""
+
+ prompt = ChatPromptTemplate.from_template(template)
+ messages = prompt.format_messages(query=query)
+ response = self.llm.invoke(messages)
+
+ return {
+ "answer": response.content,
+ "context": []
+ }
+
+def main():
+ parser = argparse.ArgumentParser(description="Query documents using OpenAI GPT-4")
+ parser.add_argument("--query", required=True, help="Query to process")
+ parser.add_argument("--store-path", default="chroma_db", help="Path to the vector store")
+ parser.add_argument("--use-cot", action="store_true", help="Enable Chain of Thought reasoning")
+ parser.add_argument("--collection", choices=["PDF Collection", "Repository Collection", "General Knowledge"],
+ help="Specify which collection to query")
+ parser.add_argument("--skip-analysis", action="store_true", help="Skip query analysis step")
+ parser.add_argument("--verbose", action="store_true", help="Show full content of sources")
+
+ args = parser.parse_args()
+
+ # Load environment variables
+ load_dotenv()
+
+ if not os.getenv("OPENAI_API_KEY"):
+ print("✗ Error: OPENAI_API_KEY not found in environment variables")
+ print("Please create a .env file with your OpenAI API key")
+ exit(1)
+
+ print("\nInitializing RAG agent...")
+ print("=" * 50)
+
+ try:
+ store = VectorStore(persist_directory=args.store_path)
+ agent = RAGAgent(
+ store,
+ openai_api_key=os.getenv("OPENAI_API_KEY"),
+ use_cot=args.use_cot,
+ collection=args.collection,
+ skip_analysis=args.skip_analysis
+ )
+
+ print(f"\nProcessing query: {args.query}")
+ print("=" * 50)
+
+ response = agent.process_query(args.query)
+
+ print("\nResponse:")
+ print("-" * 50)
+ print(response["answer"])
+
+ if response.get("reasoning_steps"):
+ print("\nReasoning Steps:")
+ print("-" * 50)
+ for i, step in enumerate(response["reasoning_steps"]):
+ print(f"\nStep {i+1}:")
+ print(step)
+
+ if response.get("context"):
+ print("\nSources used:")
+ print("-" * 50)
+
+ # Print concise list of sources
+ for i, ctx in enumerate(response["context"]):
+ source = ctx["metadata"].get("source", "Unknown")
+ if "page_numbers" in ctx["metadata"]:
+ pages = ctx["metadata"].get("page_numbers", [])
+ print(f"[{i+1}] {source} (pages: {pages})")
+ else:
+ file_path = ctx["metadata"].get("file_path", "Unknown")
+ print(f"[{i+1}] {source} (file: {file_path})")
+
+ # Only print content if verbose flag is set
+ if args.verbose:
+ content_preview = ctx["content"][:300] + "..." if len(ctx["content"]) > 300 else ctx["content"]
+ print(f" Content: {content_preview}\n")
+
+ except Exception as e:
+ print(f"\n✗ Error: {str(e)}")
+ exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/repo_processor.py b/financial/ai-agents/repo_processor.py
new file mode 100644
index 00000000..0f46327a
--- /dev/null
+++ b/financial/ai-agents/repo_processor.py
@@ -0,0 +1,176 @@
+from pathlib import Path
+from typing import List, Dict, Any, Tuple
+import json
+import argparse
+from urllib.parse import urlparse
+import warnings
+import uuid
+from gitingest import ingest
+
+def is_github_url(url: str) -> bool:
+ """Check if a string is a valid GitHub URL"""
+ try:
+ parsed = urlparse(url)
+ return parsed.netloc.lower() == "github.com"
+ except:
+ return False
+
+def extract_repo_name(repo_path: str) -> str:
+ """Extract repository name from path or URL"""
+ if is_github_url(repo_path):
+ # For GitHub URLs, extract owner/repo format
+ parts = repo_path.rstrip('/').split('/')
+ if len(parts) >= 5:
+ return f"{parts[3]}/{parts[4]}" # owner/repo format
+
+ # For local paths, use the last directory name
+ return Path(repo_path).name
+
+class RepoProcessor:
+ def __init__(self, chunk_size: int = 500):
+ """Initialize repository processor with chunk size"""
+ self.chunk_size = chunk_size
+
+ def _extract_metadata(self, summary: Dict[str, Any], tree: Dict[str, Any], repo_path: str) -> Dict[str, Any]:
+ """Extract metadata from repository summary and tree"""
+ # Extract repo name from path or URL
+ repo_name = extract_repo_name(repo_path)
+
+ # Handle case where summary might be a string
+ if isinstance(summary, str):
+ return {
+ "repo_name": repo_name,
+ "file_count": len(tree) if tree else 0
+ }
+
+ return {
+ "repo_name": repo_name, # Use extracted name instead of summary
+ "file_count": len(tree) if tree else 0
+ }
+
+ def _chunk_text(self, text: str) -> List[str]:
+ """Split text into chunks of roughly equal size"""
+ # Split into sentences (roughly)
+ sentences = [s.strip() for s in text.split('.') if s.strip()]
+
+ chunks = []
+ current_chunk = []
+ current_length = 0
+
+ for sentence in sentences:
+ # Add period back
+ sentence = sentence + '.'
+ # If adding this sentence would exceed chunk size, save current chunk
+ if current_length + len(sentence) > self.chunk_size and current_chunk:
+ chunks.append(' '.join(current_chunk))
+ current_chunk = []
+ current_length = 0
+
+ current_chunk.append(sentence)
+ current_length += len(sentence)
+
+ # Add any remaining text
+ if current_chunk:
+ chunks.append(' '.join(current_chunk))
+
+ return chunks
+
+ def process_repo(self, repo_path: str | Path) -> Tuple[List[Dict[str, Any]], str]:
+ """Process a repository and return chunks of text with metadata"""
+ try:
+ # Generate a unique document ID
+ document_id = str(uuid.uuid4())
+
+ # Check if it's a GitHub URL
+ if isinstance(repo_path, str) and is_github_url(repo_path):
+ print(f"Processing GitHub repository: {repo_path}")
+ else:
+ print(f"Processing local repository: {repo_path}")
+
+ # Ingest repository
+ summary, tree, content = ingest(str(repo_path))
+
+ # Extract metadata
+ metadata = self._extract_metadata(summary, tree, str(repo_path))
+
+ # Process content into chunks
+ processed_chunks = []
+ chunk_id = 0
+
+ if isinstance(content, dict):
+ # Handle dictionary of file contents
+ for file_path, file_content in content.items():
+ if isinstance(file_content, str) and file_content.strip(): # Only process non-empty content
+ # Split content into chunks
+ text_chunks = self._chunk_text(file_content)
+
+ for text_chunk in text_chunks:
+ chunk = {
+ "text": text_chunk,
+ "metadata": {
+ **metadata,
+ "source": str(repo_path),
+ "document_id": document_id,
+ "chunk_id": chunk_id
+ }
+ }
+ processed_chunks.append(chunk)
+ chunk_id += 1
+ elif isinstance(content, str):
+ # Handle single string content
+ text_chunks = self._chunk_text(content)
+
+ for text_chunk in text_chunks:
+ chunk = {
+ "text": text_chunk,
+ "metadata": {
+ **metadata,
+ "source": str(repo_path),
+ "document_id": document_id,
+ "chunk_id": chunk_id
+ }
+ }
+ processed_chunks.append(chunk)
+ chunk_id += 1
+
+ return processed_chunks, document_id
+
+ except Exception as e:
+ raise Exception(f"Error processing repository {repo_path}: {str(e)}")
+
+def main():
+ parser = argparse.ArgumentParser(description="Process GitHub repositories and extract content")
+ parser.add_argument("--input", required=True,
+ help="Input repository path or GitHub URL")
+ parser.add_argument("--output", required=True, help="Output JSON file for chunks")
+ parser.add_argument("--chunk-size", type=int, default=500,
+ help="Maximum size of text chunks")
+
+ args = parser.parse_args()
+ processor = RepoProcessor(chunk_size=args.chunk_size)
+
+ try:
+ # Create output directory if it doesn't exist
+ output_dir = Path(args.output).parent
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ print(f"\nProcessing repository: {args.input}")
+ print("=" * 50)
+
+ chunks, doc_id = processor.process_repo(args.input)
+
+ # Save chunks to JSON
+ with open(args.output, 'w', encoding='utf-8') as f:
+ json.dump(chunks, f, ensure_ascii=False, indent=2)
+
+ print("\nSummary:")
+ print(f"✓ Processed {len(chunks)} chunks")
+ print(f"✓ Document ID: {doc_id}")
+ print(f"✓ Saved to {args.output}")
+
+ except Exception as e:
+ print(f"\n✗ Error: {str(e)}")
+ exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/requirements.txt b/financial/ai-agents/requirements.txt
new file mode 100644
index 00000000..6b6af436
--- /dev/null
+++ b/financial/ai-agents/requirements.txt
@@ -0,0 +1,22 @@
+docling
+langchain-openai
+chromadb
+python-dotenv
+openai
+pydantic
+fastapi
+uvicorn
+python-multipart
+transformers
+torch
+accelerate
+pyyaml
+trafilatura
+gradio
+lxml_html_clean
+langchain
+gitingest
+bitsandbytes
+ollama
+oracledb
+sentence-transformers
\ No newline at end of file
diff --git a/financial/ai-agents/store.py b/financial/ai-agents/store.py
new file mode 100644
index 00000000..94e7c0f0
--- /dev/null
+++ b/financial/ai-agents/store.py
@@ -0,0 +1,242 @@
+from typing import List, Dict, Any
+import chromadb
+import json
+import argparse
+from chromadb.config import Settings
+
+class VectorStore:
+ def __init__(self, persist_directory: str = "embeddings"):
+ """Initialize vector store with ChromaDB"""
+ self.client = chromadb.PersistentClient(
+ path=persist_directory,
+ settings=Settings(allow_reset=True)
+ )
+
+ # Create or get collections
+ self.pdf_collection = self.client.get_or_create_collection(
+ name="pdf_documents",
+ metadata={"hnsw:space": "cosine"}
+ )
+ self.web_collection = self.client.get_or_create_collection(
+ name="web_documents",
+ metadata={"hnsw:space": "cosine"}
+ )
+ self.repo_collection = self.client.get_or_create_collection(
+ name="repository_documents",
+ metadata={"hnsw:space": "cosine"}
+ )
+ self.general_collection = self.client.get_or_create_collection(
+ name="general_knowledge",
+ metadata={"hnsw:space": "cosine"}
+ )
+
+ def _sanitize_metadata(self, metadata: Dict) -> Dict:
+ """Sanitize metadata to ensure all values are valid types for ChromaDB"""
+ sanitized = {}
+ for key, value in metadata.items():
+ if isinstance(value, (str, int, float, bool)):
+ sanitized[key] = value
+ elif isinstance(value, list):
+ # Convert list to string representation
+ sanitized[key] = str(value)
+ elif value is None:
+ # Replace None with empty string
+ sanitized[key] = ""
+ else:
+ # Convert any other type to string
+ sanitized[key] = str(value)
+ return sanitized
+
+ def add_pdf_chunks(self, chunks: List[Dict[str, Any]], document_id: str):
+ """Add chunks from a PDF document to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for ChromaDB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{document_id}_{i}" for i in range(len(chunks))]
+
+ # Add to collection
+ self.pdf_collection.add(
+ documents=texts,
+ metadatas=metadatas,
+ ids=ids
+ )
+
+ def add_web_chunks(self, chunks: List[Dict[str, Any]], source_id: str):
+ """Add chunks from web content to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for ChromaDB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{source_id}_{i}" for i in range(len(chunks))]
+
+ # Add to collection
+ self.web_collection.add(
+ documents=texts,
+ metadatas=metadatas,
+ ids=ids
+ )
+
+ def add_general_knowledge(self, chunks: List[Dict[str, Any]], source_id: str):
+ """Add general knowledge chunks to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for ChromaDB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{source_id}_{i}" for i in range(len(chunks))]
+
+ # Add to collection
+ self.general_collection.add(
+ documents=texts,
+ metadatas=metadatas,
+ ids=ids
+ )
+
+ def add_repo_chunks(self, chunks: List[Dict[str, Any]], document_id: str):
+ """Add chunks from a repository to the vector store"""
+ if not chunks:
+ return
+
+ # Prepare data for ChromaDB
+ texts = [chunk["text"] for chunk in chunks]
+ metadatas = [self._sanitize_metadata(chunk["metadata"]) for chunk in chunks]
+ ids = [f"{document_id}_{i}" for i in range(len(chunks))]
+
+ # Add to collection
+ self.repo_collection.add(
+ documents=texts,
+ metadatas=metadatas,
+ ids=ids
+ )
+
+ def query_pdf_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the PDF documents collection"""
+ print("📊 [ChromaDB] Querying PDF Collection")
+ results = self.pdf_collection.query(
+ query_texts=[query],
+ n_results=n_results
+ )
+
+ # Format results
+ formatted_results = []
+ for i in range(len(results["documents"][0])):
+ result = {
+ "content": results["documents"][0][i],
+ "metadata": results["metadatas"][0][i]
+ }
+ formatted_results.append(result)
+
+ print(f"📊 [ChromaDB] Retrieved {len(formatted_results)} chunks from PDF Collection")
+ return formatted_results
+
+ def query_web_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the web documents collection"""
+ print("📊 [ChromaDB] Querying Web Collection")
+ results = self.web_collection.query(
+ query_texts=[query],
+ n_results=n_results
+ )
+
+ # Format results
+ formatted_results = []
+ for i in range(len(results["documents"][0])):
+ result = {
+ "content": results["documents"][0][i],
+ "metadata": results["metadatas"][0][i]
+ }
+ formatted_results.append(result)
+
+ print(f"📊 [ChromaDB] Retrieved {len(formatted_results)} chunks from Web Collection")
+ return formatted_results
+
+ def query_general_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the general knowledge collection"""
+ print("📊 [ChromaDB] Querying General Knowledge Collection")
+ results = self.general_collection.query(
+ query_texts=[query],
+ n_results=n_results
+ )
+
+ # Format results
+ formatted_results = []
+ for i in range(len(results["documents"][0])):
+ result = {
+ "content": results["documents"][0][i],
+ "metadata": results["metadatas"][0][i]
+ }
+ formatted_results.append(result)
+
+ print(f"📊 [ChromaDB] Retrieved {len(formatted_results)} chunks from General Knowledge Collection")
+ return formatted_results
+
+ def query_repo_collection(self, query: str, n_results: int = 3) -> List[Dict[str, Any]]:
+ """Query the repository documents collection"""
+ print("📊 [ChromaDB] Querying Repository Collection")
+ results = self.repo_collection.query(
+ query_texts=[query],
+ n_results=n_results
+ )
+
+ # Format results
+ formatted_results = []
+ for i in range(len(results["documents"][0])):
+ result = {
+ "content": results["documents"][0][i],
+ "metadata": results["metadatas"][0][i]
+ }
+ formatted_results.append(result)
+
+ print(f"📊 [ChromaDB] Retrieved {len(formatted_results)} chunks from Repository Collection")
+ return formatted_results
+
+def main():
+ parser = argparse.ArgumentParser(description="Manage vector store")
+ parser.add_argument("--add", help="JSON file containing chunks to add")
+ parser.add_argument("--add-web", help="JSON file containing web chunks to add")
+ parser.add_argument("--query", help="Query to search for")
+ parser.add_argument("--store-path", default="embeddings", help="Path to vector store")
+
+ args = parser.parse_args()
+ store = VectorStore(persist_directory=args.store_path)
+
+ if args.add:
+ with open(args.add, 'r', encoding='utf-8') as f:
+ chunks = json.load(f)
+ store.add_pdf_chunks(chunks, document_id=args.add)
+ print(f"✓ Added {len(chunks)} PDF chunks to vector store")
+
+ if args.add_web:
+ with open(args.add_web, 'r', encoding='utf-8') as f:
+ chunks = json.load(f)
+ store.add_web_chunks(chunks, source_id=args.add_web)
+ print(f"✓ Added {len(chunks)} web chunks to vector store")
+
+ if args.query:
+ # Query both collections
+ pdf_results = store.query_pdf_collection(args.query)
+ web_results = store.query_web_collection(args.query)
+
+ print("\nPDF Results:")
+ print("-" * 50)
+ for result in pdf_results:
+ print(f"Content: {result['content'][:200]}...")
+ print(f"Source: {result['metadata'].get('source', 'Unknown')}")
+ print(f"Pages: {result['metadata'].get('page_numbers', [])}")
+ print("-" * 50)
+
+ print("\nWeb Results:")
+ print("-" * 50)
+ for result in web_results:
+ print(f"Content: {result['content'][:200]}...")
+ print(f"Source: {result['metadata'].get('source', 'Unknown')}")
+ print(f"Title: {result['metadata'].get('title', 'Unknown')}")
+ print("-" * 50)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/test_cot.py b/financial/ai-agents/test_cot.py
new file mode 100644
index 00000000..3a600684
--- /dev/null
+++ b/financial/ai-agents/test_cot.py
@@ -0,0 +1,105 @@
+import sys
+import os
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from local_rag_agent import LocalRAGAgent
+from rag_agent import RAGAgent
+from store import VectorStore
+from dotenv import load_dotenv
+import yaml
+import argparse
+from rich.console import Console
+from rich.panel import Panel
+from rich.table import Table
+
+console = Console()
+
+def load_config():
+ """Load configuration from config.yaml and .env"""
+ try:
+ with open('config.yaml', 'r') as f:
+ config = yaml.safe_load(f)
+ load_dotenv()
+ return {
+ 'hf_token': config.get('HUGGING_FACE_HUB_TOKEN'),
+ 'openai_key': os.getenv('OPENAI_API_KEY')
+ }
+ except Exception as e:
+ console.print(f"[red]Error loading configuration: {str(e)}")
+ sys.exit(1)
+
+def compare_responses(agent, query: str, description: str):
+ """Compare standard vs CoT responses for the same query"""
+ console.print(f"\n[bold cyan]Test Case: {description}")
+ console.print(Panel(f"Query: {query}", style="yellow"))
+
+ # Standard response
+ agent.use_cot = False
+ standard_response = agent.process_query(query)
+ console.print(Panel(
+ "[bold]Standard Response:[/bold]\n" + standard_response["answer"],
+ title="Without Chain of Thought",
+ style="blue"
+ ))
+
+ # CoT response
+ agent.use_cot = True
+ cot_response = agent.process_query(query)
+ console.print(Panel(
+ "[bold]Chain of Thought Response:[/bold]\n" + cot_response["answer"],
+ title="With Chain of Thought",
+ style="green"
+ ))
+
+def main():
+ parser = argparse.ArgumentParser(description="Compare standard vs Chain of Thought prompting")
+ parser.add_argument("--model", choices=['local', 'openai'], default='local',
+ help="Choose between local Mistral model or OpenAI")
+ args = parser.parse_args()
+
+ config = load_config()
+ store = VectorStore(persist_directory="chroma_db")
+
+ # Initialize appropriate agent
+ if args.model == 'local':
+ if not config['hf_token']:
+ console.print("[red]Error: HuggingFace token not found in config.yaml")
+ sys.exit(1)
+ agent = LocalRAGAgent(store)
+ model_name = "Mistral-7B"
+ else:
+ if not config['openai_key']:
+ console.print("[red]Error: OpenAI API key not found in .env")
+ sys.exit(1)
+ agent = RAGAgent(store, openai_api_key=config['openai_key'])
+ model_name = "GPT-4"
+
+ console.print(f"\n[bold]Testing {model_name} Responses[/bold]")
+ console.print("=" * 80)
+
+ # Test cases that highlight CoT benefits
+ test_cases = [
+ {
+ "query": "A train travels at 60 mph for 2.5 hours, then at 45 mph for 1.5 hours. What's the total distance covered?",
+ "description": "Multi-step math problem"
+ },
+ {
+ "query": "Compare and contrast REST and GraphQL APIs, considering their strengths and use cases.",
+ "description": "Complex comparison requiring structured analysis"
+ },
+ {
+ "query": "If a tree falls in a forest and no one is around to hear it, does it make a sound? Explain your reasoning.",
+ "description": "Philosophical question requiring detailed reasoning"
+ }
+ ]
+
+ for test_case in test_cases:
+ try:
+ compare_responses(agent, test_case["query"], test_case["description"])
+ except Exception as e:
+ console.print(f"[red]Error in test case '{test_case['description']}': {str(e)}")
+
+ console.print("\n[bold green]Testing complete!")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/test_db_systems.sh b/financial/ai-agents/test_db_systems.sh
new file mode 100644
index 00000000..a90a9e29
--- /dev/null
+++ b/financial/ai-agents/test_db_systems.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# Script to test both Oracle DB and ChromaDB for the Personalized Investment Report Generation (AI Agents, Vector Search, MCP, langgraph)
+
+echo "===== Agentic RAG Database Systems Test ====="
+echo
+
+# Check for required packages
+echo "Checking for required Python packages..."
+pip list | grep -E "oracledb|sentence-transformers|chromadb" || echo "Some required packages may be missing"
+
+echo
+echo "===== Testing Oracle DB ====="
+echo "Running Oracle DB connection test..."
+python test_oradb.py
+
+echo
+echo "===== Testing ChromaDB ====="
+echo "Creating a test query using ChromaDB as fallback..."
+python -c '
+from local_rag_agent import LocalRAGAgent
+agent = LocalRAGAgent(use_oracle_db=False)
+print("ChromaDB initialized successfully")
+print("Querying with test prompt...")
+result = agent.process_query("What is machine learning?")
+print(f"Response generated successfully. Length: {len(result['answer'])}")
+'
+
+echo
+echo "===== Testing Done ====="
+echo "If both tests passed, your system is correctly configured for dual database support."
+echo "Oracle DB will be used by default, with ChromaDB as fallback."
\ No newline at end of file
diff --git a/financial/ai-agents/test_new_cot.py b/financial/ai-agents/test_new_cot.py
new file mode 100644
index 00000000..4f25cabe
--- /dev/null
+++ b/financial/ai-agents/test_new_cot.py
@@ -0,0 +1,118 @@
+import sys
+import os
+import argparse
+from dotenv import load_dotenv
+from rich.console import Console
+from rich.panel import Panel
+from store import VectorStore
+from rag_agent import RAGAgent
+from local_rag_agent import LocalRAGAgent
+import yaml
+
+# Configure rich console
+console = Console()
+
+def test_multi_agent_cot(agent, query: str, description: str):
+ """Test the multi-agent Chain of Thought system"""
+ console.print(f"\n[bold cyan]Test Case: {description}")
+ console.print(Panel(f"Query: {query}", style="yellow"))
+
+ # Process query with multi-agent CoT
+ response = agent.process_query(query)
+
+ # Print each step's result
+ if response.get("reasoning_steps"):
+ for i, step in enumerate(response["reasoning_steps"]):
+ console.print(Panel(
+ f"[bold]Step {i+1}:[/bold]\n{step}",
+ title=f"Reasoning Step {i+1}",
+ style="blue"
+ ))
+
+ # Print final answer
+ console.print(Panel(
+ f"[bold]Final Answer:[/bold]\n{response['answer']}",
+ title="Synthesized Response",
+ style="green"
+ ))
+
+ # Print sources if available
+ if response.get("context"):
+ console.print("\n[bold]Sources Used:[/bold]")
+ for ctx in response["context"]:
+ source = ctx["metadata"].get("source", "Unknown")
+ if "page_numbers" in ctx["metadata"]:
+ pages = ctx["metadata"].get("page_numbers", [])
+ console.print(f"- {source} (pages: {pages})")
+ else:
+ file_path = ctx["metadata"].get("file_path", "Unknown")
+ console.print(f"- {source} (file: {file_path})")
+
+def main():
+ parser = argparse.ArgumentParser(description="Test multi-agent Chain of Thought reasoning")
+ parser.add_argument("--model", choices=['local', 'openai'], default='local',
+ help="Choose between local Mistral model or OpenAI (default: local)")
+ parser.add_argument("--store-path", default="chroma_db", help="Path to the vector store")
+ args = parser.parse_args()
+
+ # Load environment variables and config
+ load_dotenv()
+
+ try:
+ with open('config.yaml', 'r') as f:
+ config = yaml.safe_load(f)
+ hf_token = config.get('HUGGING_FACE_HUB_TOKEN')
+ except Exception:
+ hf_token = None
+
+ console.print("\n[bold]Testing Multi-Agent Chain of Thought System[/bold]")
+ console.print("=" * 80)
+
+ try:
+ # Initialize vector store
+ store = VectorStore(persist_directory=args.store_path)
+
+ # Initialize appropriate agent
+ if args.model == 'local':
+ if not hf_token:
+ console.print("[red]Error: HuggingFace token not found in config.yaml")
+ sys.exit(1)
+ agent = LocalRAGAgent(store, use_cot=True)
+ model_name = "Mistral-7B"
+ else:
+ if not os.getenv("OPENAI_API_KEY"):
+ console.print("[red]Error: OpenAI API key not found in .env")
+ sys.exit(1)
+ agent = RAGAgent(store, openai_api_key=os.getenv("OPENAI_API_KEY"), use_cot=True)
+ model_name = "GPT-4"
+
+ console.print(f"\n[bold]Using {model_name} with Multi-Agent CoT[/bold]")
+ console.print("=" * 80)
+
+ # Test cases that demonstrate multi-agent CoT benefits
+ test_cases = [
+ {
+ "query": "What are the key differences between traditional RAG systems and the agentic RAG approach implemented in this project?",
+ "description": "Complex comparison requiring analysis of implementation details"
+ },
+ {
+ "query": "How does the system handle PDF document processing and what are the main steps involved?",
+ "description": "Technical process analysis requiring step-by-step breakdown"
+ },
+ {
+ "query": "What are the advantages and limitations of using local LLMs versus cloud-based models in this implementation?",
+ "description": "Trade-off analysis requiring multiple perspectives"
+ }
+ ]
+
+ # Run test cases
+ for test_case in test_cases:
+ test_multi_agent_cot(agent, test_case["query"], test_case["description"])
+ console.print("\n" + "=" * 80)
+
+ except Exception as e:
+ console.print(f"\n[red]Error: {str(e)}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/test_oradb.py b/financial/ai-agents/test_oradb.py
new file mode 100644
index 00000000..d4f8c810
--- /dev/null
+++ b/financial/ai-agents/test_oradb.py
@@ -0,0 +1,214 @@
+import argparse
+import json
+from OraDBVectorStore import OraDBVectorStore
+import time
+import sys
+import yaml
+from pathlib import Path
+
+def check_credentials():
+ """Check if Oracle DB credentials are configured in config.yaml"""
+ try:
+ config_path = Path("config.yaml")
+ if not config_path.exists():
+ print("✗ config.yaml not found.")
+ return False
+
+ with open(config_path, 'r') as f:
+ config = yaml.safe_load(f)
+
+ if not config:
+ print("✗ config.yaml is empty or invalid YAML.")
+ return False
+
+ # Check for Oracle DB credentials
+ if not config.get("ORACLE_DB_USERNAME"):
+ print("✗ ORACLE_DB_USERNAME not found in config.yaml")
+ return False
+
+ if not config.get("ORACLE_DB_PASSWORD"):
+ print("✗ ORACLE_DB_PASSWORD not found in config.yaml")
+ return False
+
+ if not config.get("ORACLE_DB_DSN"):
+ print("✗ ORACLE_DB_DSN not found in config.yaml")
+ return False
+
+ print("✓ Oracle DB credentials found in config.yaml")
+ return True
+ except Exception as e:
+ print(f"✗ Error checking credentials: {str(e)}")
+ return False
+
+def test_connection():
+ """Test connection to Oracle DB"""
+ print("Testing Oracle DB connection...")
+ try:
+ store = OraDBVectorStore()
+ print("✓ Connection successful!")
+ return store
+ except Exception as e:
+ print(f"✗ Connection failed: {str(e)}")
+ return None
+
+def check_collection_stats(store):
+ """Check statistics for each collection including total chunks and latest insertion"""
+ if not store:
+ print("Skipping collection stats check as connection failed")
+ return
+
+ print("\n=== Collection Statistics ===")
+
+ collections = {
+ "PDF Collection": "pdf_documents",
+ "Repository Collection": "repository_documents",
+ "Web Knowledge Base": "web_documents",
+ "General Knowledge": "general_knowledge"
+ }
+
+ for name, collection in collections.items():
+ try:
+ # Get total count
+ count = store.get_collection_count(collection)
+ print(f"\n{name}:")
+ print(f"Total chunks: {count}")
+
+ # Get latest insertion if collection is not empty
+ if count > 0:
+ latest = store.get_latest_chunk(collection)
+ print("Latest chunk:")
+ print(f" Content: {latest['content'][:150]}..." if len(latest['content']) > 150 else f" Content: {latest['content']}")
+
+ # Print metadata
+ if isinstance(latest['metadata'], str):
+ try:
+ metadata = json.loads(latest['metadata'])
+ except:
+ metadata = {"source": latest['metadata']}
+ else:
+ metadata = latest['metadata']
+
+ source = metadata.get('source', 'Unknown')
+ print(f" Source: {source}")
+
+ # Print other metadata based on collection type
+ if collection == "pdf_documents" and 'page' in metadata:
+ print(f" Page: {metadata['page']}")
+ elif collection == "repository_documents" and 'file_path' in metadata:
+ print(f" File: {metadata['file_path']}")
+ elif collection == "web_documents" and 'title' in metadata:
+ print(f" Title: {metadata['title']}")
+ else:
+ print("No chunks found in this collection.")
+
+ except Exception as e:
+ print(f"Error checking {name}: {str(e)}")
+
+def test_add_and_query(store, query_text="machine learning"):
+ """Test adding simple data and querying it"""
+ if not store:
+ print("Skipping add and query test as connection failed")
+ return
+
+ print("\nTesting add and query functionality...")
+
+ # Create simple test document
+ test_chunks = [
+ {
+ "text": "Machine learning is a field of study in artificial intelligence concerned with the development of algorithms that can learn from data.",
+ "metadata": {
+ "source": "Test Document",
+ "page": 1
+ }
+ },
+ {
+ "text": "Deep learning is a subset of machine learning that uses neural networks with many layers.",
+ "metadata": {
+ "source": "Test Document",
+ "page": 2
+ }
+ }
+ ]
+
+ try:
+ # Test adding PDF chunks
+ print("Adding test chunks to PDF collection...")
+ store.add_pdf_chunks(test_chunks, document_id="test_document")
+ print("✓ Successfully added test chunks")
+
+ # Test querying
+ print(f"\nQuerying with: '{query_text}'")
+ start_time = time.time()
+ results = store.query_pdf_collection(query_text)
+ query_time = time.time() - start_time
+
+ print(f"✓ Query completed in {query_time:.2f} seconds")
+ print(f"Found {len(results)} results")
+
+ # Display results
+ if results:
+ print("\nResults:")
+ for i, result in enumerate(results):
+ print(f"\nResult {i+1}:")
+ print(f"Content: {result['content']}")
+ print(f"Source: {result['metadata'].get('source', 'Unknown')}")
+ print(f"Page: {result['metadata'].get('page', 'Unknown')}")
+ else:
+ print("No results found.")
+
+ except Exception as e:
+ print(f"✗ Test failed: {str(e)}")
+
+def main():
+ parser = argparse.ArgumentParser(description="Test Oracle DB Vector Store")
+ parser.add_argument("--query", default="machine learning", help="Query to use for testing")
+ parser.add_argument("--stats-only", action="store_true", help="Only show collection statistics without inserting test data")
+
+ args = parser.parse_args()
+
+ print("=== Oracle DB Vector Store Test ===\n")
+
+ # Check if oracledb is installed
+ try:
+ import oracledb
+ print("✓ oracledb package is installed")
+ except ImportError:
+ print("✗ oracledb package is not installed.")
+ print("Please install it with: pip install oracledb")
+ sys.exit(1)
+
+ # Check if sentence_transformers is installed
+ try:
+ import sentence_transformers
+ print("✓ sentence_transformers package is installed")
+ except ImportError:
+ print("✗ sentence_transformers package is not installed.")
+ print("Please install it with: pip install sentence-transformers")
+ sys.exit(1)
+
+ # Check if credentials are configured
+ if not check_credentials():
+ print("\n✗ Oracle DB credentials not properly configured in config.yaml")
+ print("Please update config.yaml with the following:")
+ print("""
+ORACLE_DB_USERNAME: ADMIN
+ORACLE_DB_PASSWORD: your_password_here
+ORACLE_DB_DSN: your_connection_string_here
+ """)
+ sys.exit(1)
+
+ # Test connection
+ store = test_connection()
+
+ # Check collection statistics
+ check_collection_stats(store)
+
+ # If stats-only flag is not set, also test add and query functionality
+ if not args.stats_only:
+ # Test add and query functionality
+ test_add_and_query(store, args.query)
+
+ print("\n=== Test Completed ===")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-agents/web_processor.py b/financial/ai-agents/web_processor.py
new file mode 100644
index 00000000..4927cfd0
--- /dev/null
+++ b/financial/ai-agents/web_processor.py
@@ -0,0 +1,218 @@
+from pathlib import Path
+import json
+import argparse
+from typing import List, Dict, Any
+from trafilatura import fetch_url, extract, extract_metadata
+from urllib.parse import urlparse
+import re
+
+def is_url(string: str) -> bool:
+ """Check if a string is a valid URL"""
+ try:
+ result = urlparse(string)
+ return all([result.scheme, result.netloc])
+ except:
+ return False
+
+def get_domain(url: str) -> str:
+ """Extract domain from URL"""
+ parsed = urlparse(url)
+ return parsed.netloc.lower()
+
+class WebProcessor:
+ def __init__(self, chunk_size: int = 500):
+ """Initialize web processor with chunk size"""
+ self.chunk_size = chunk_size
+ # Define domains that need special handling
+ self.special_domains = {
+ 'x.com': 'twitter',
+ 'twitter.com': 'twitter',
+ 'github.com': 'github'
+ }
+
+ def _handle_twitter(self, url: str) -> Dict[str, Any]:
+ """Special handling for Twitter/X URLs"""
+ # Extract tweet ID from URL
+ tweet_id = url.split('/')[-1]
+ return {
+ 'text': f"Twitter/X content (Tweet ID: {tweet_id}). Note: Twitter content cannot be directly extracted. Please visit {url} to view the content.",
+ 'metadata': {
+ 'source': url,
+ 'type': 'twitter',
+ 'tweet_id': tweet_id
+ }
+ }
+
+ def _handle_github(self, url: str) -> Dict[str, Any]:
+ """Special handling for GitHub URLs"""
+ # Extract repo info from URL
+ parts = url.split('/')
+ if len(parts) >= 5:
+ owner = parts[3]
+ repo = parts[4]
+ return {
+ 'text': f"GitHub Repository: {owner}/{repo}. This is a GitHub repository. For better results, try accessing specific files or the README directly.",
+ 'metadata': {
+ 'source': url,
+ 'type': 'github',
+ 'owner': owner,
+ 'repo': repo
+ }
+ }
+ return None
+
+ def _chunk_text(self, text: str) -> List[str]:
+ """Split text into chunks of roughly equal size"""
+ # Split into sentences (roughly)
+ sentences = [s.strip() for s in text.split('.') if s.strip()]
+
+ chunks = []
+ current_chunk = []
+ current_length = 0
+
+ for sentence in sentences:
+ # Add period back
+ sentence = sentence + '.'
+ # If adding this sentence would exceed chunk size, save current chunk
+ if current_length + len(sentence) > self.chunk_size and current_chunk:
+ chunks.append(' '.join(current_chunk))
+ current_chunk = []
+ current_length = 0
+
+ current_chunk.append(sentence)
+ current_length += len(sentence)
+
+ # Add any remaining text
+ if current_chunk:
+ chunks.append(' '.join(current_chunk))
+
+ return chunks
+
+ def process_url(self, url: str) -> List[Dict[str, Any]]:
+ """Process a URL and return chunks of text with metadata"""
+ try:
+ domain = get_domain(url)
+
+ # Check if this domain needs special handling
+ if domain in self.special_domains:
+ handler = getattr(self, f"_handle_{self.special_domains[domain]}", None)
+ if handler:
+ result = handler(url)
+ if result:
+ return [{
+ "text": result["text"],
+ "metadata": result["metadata"]
+ }]
+
+ # Standard processing for other domains
+ downloaded = fetch_url(url)
+ if not downloaded:
+ raise ValueError(f"Failed to fetch URL: {url}")
+
+ # Extract text and metadata
+ text = extract(downloaded, include_comments=False, include_tables=False)
+ try:
+ metadata = extract_metadata(downloaded)
+ # Convert metadata to dict if it's not already
+ if not isinstance(metadata, dict):
+ metadata = {
+ 'title': getattr(metadata, 'title', ''),
+ 'author': getattr(metadata, 'author', ''),
+ 'date': getattr(metadata, 'date', ''),
+ 'sitename': getattr(metadata, 'sitename', ''),
+ 'categories': getattr(metadata, 'categories', []),
+ 'tags': getattr(metadata, 'tags', [])
+ }
+ except Exception as e:
+ print(f"Warning: Metadata extraction failed: {str(e)}")
+ metadata = {}
+
+ if not text:
+ raise ValueError(f"No text content extracted from URL: {url}. This might be due to:\n" +
+ "1. Website blocking automated access\n" +
+ "2. Content requiring JavaScript\n" +
+ "3. Content behind authentication\n" +
+ "4. Website using non-standard HTML structure")
+
+ # Split into chunks
+ text_chunks = self._chunk_text(text)
+
+ # Process chunks into a standardized format
+ processed_chunks = []
+ for i, chunk in enumerate(text_chunks):
+ processed_chunk = {
+ "text": chunk,
+ "metadata": {
+ "source": url,
+ "title": metadata.get('title', ''),
+ "author": metadata.get('author', ''),
+ "date": metadata.get('date', ''),
+ "sitename": metadata.get('sitename', ''),
+ "categories": metadata.get('categories', []),
+ "tags": metadata.get('tags', []),
+ "chunk_id": i,
+ "type": "webpage"
+ }
+ }
+ processed_chunks.append(processed_chunk)
+
+ return processed_chunks
+
+ except Exception as e:
+ raise Exception(f"Error processing URL {url}: {str(e)}")
+
+ def process_urls(self, urls: List[str]) -> List[Dict[str, Any]]:
+ """Process multiple URLs and return combined chunks"""
+ all_chunks = []
+
+ for url in urls:
+ try:
+ chunks = self.process_url(url)
+ all_chunks.extend(chunks)
+ print(f"✓ Processed {url}")
+ except Exception as e:
+ print(f"✗ Failed to process {url}: {str(e)}")
+
+ return all_chunks
+
+def main():
+ parser = argparse.ArgumentParser(description="Process web pages and extract text chunks")
+ parser.add_argument("--input", required=True, help="Input URL or file containing URLs (one per line)")
+ parser.add_argument("--output", required=True, help="Output JSON file for chunks")
+ parser.add_argument("--chunk-size", type=int, default=500, help="Maximum size of text chunks")
+
+ args = parser.parse_args()
+ processor = WebProcessor(chunk_size=args.chunk_size)
+
+ try:
+ # Create output directory if it doesn't exist
+ output_dir = Path(args.output).parent
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ if is_url(args.input):
+ print(f"\nProcessing URL: {args.input}")
+ print("=" * 50)
+ chunks = processor.process_url(args.input)
+ else:
+ # Read URLs from file
+ with open(args.input, 'r', encoding='utf-8') as f:
+ urls = [line.strip() for line in f if line.strip()]
+
+ print(f"\nProcessing {len(urls)} URLs from: {args.input}")
+ print("=" * 50)
+ chunks = processor.process_urls(urls)
+
+ # Save chunks to JSON
+ with open(args.output, 'w', encoding='utf-8') as f:
+ json.dump(chunks, f, ensure_ascii=False, indent=2)
+
+ print("\nSummary:")
+ print(f"✓ Processed {len(chunks)} chunks")
+ print(f"✓ Saved to {args.output}")
+
+ except Exception as e:
+ print(f"\n✗ Error: {str(e)}")
+ exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/financial/ai-services/Dockerfile b/financial/ai-services/Dockerfile
new file mode 100755
index 00000000..42594586
--- /dev/null
+++ b/financial/ai-services/Dockerfile
@@ -0,0 +1,13 @@
+# Stage 1: Build the app
+FROM --platform=linux/amd64 maven:3.9.4-eclipse-temurin-17 AS maven_build
+WORKDIR /build
+COPY pom.xml .
+COPY src ./src
+RUN mvn clean package -DskipTests
+
+# Stage 2: Run the app
+FROM --platform=linux/amd64 eclipse-temurin:17-jre
+WORKDIR /app
+COPY --from=maven_build /build/target/*.jar app.jar
+EXPOSE 8080
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/financial/ai-services/all.sh b/financial/ai-services/all.sh
new file mode 100755
index 00000000..365d98e8
--- /dev/null
+++ b/financial/ai-services/all.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+./build.sh
+./deploy.sh
+echo logpod front
+logpod front
diff --git a/financial/ai-services/build.sh b/financial/ai-services/build.sh
new file mode 100755
index 00000000..0b288a98
--- /dev/null
+++ b/financial/ai-services/build.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+export IMAGE_NAME=frontend
+export IMAGE_VERSION=0.4
+export DOCKER_REGISTRY=us-ashburn-1.ocir.io/oradbclouducm/financial
+#export DOCKER_REGISTRY0=us-ashburn-1.ocir.io/oradbclouducm/gd35252210
+
+if [ -z "$DOCKER_REGISTRY" ]; then
+ echo "Error: DOCKER_REGISTRY env variable needs to be set!"
+ exit 1
+fi
+
+export IMAGE=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_VERSION}
+echo ${IMAGE}
+
+#oci artifacts container repository create --compartment-id ocid1.compartment.oc1..aaaaaaaafnah3ogykjsg34qruhixhb2drls6zhsejzm7mubi2i5qj66slcoq --display-name financial/frontend --is-public true
+
+
+#mvn clean package spring-boot:repackage
+mvn clean package
+
+echo about to build...
+#podman build -t=$IMAGE .
+podman buildx build --platform linux/amd64 -t $IMAGE .
+#podman buildx build --platform linux/amd64 -t $IMAGE --load .
+
+
+podman push "$IMAGE"
+
+#podman run --rm -p 8080:8080 $IMAGE
+# podman run --rm -p 8080:8080 us-ashburn-1.ocir.io/oradbclouducm/financial/frontend:0.1
+
diff --git a/financial/ai-services/deploy.sh b/financial/ai-services/deploy.sh
new file mode 100755
index 00000000..49b56bfc
--- /dev/null
+++ b/financial/ai-services/deploy.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+## Copyright (c) 2021 Oracle and/or its affiliates.
+## Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
+
+
+#kubectl apply -f frontend-springboot-service.yaml -n financial
+kubectl apply -f frontend-loadbalancer-service.yaml -n financial
+kubectl delete deployment frontend-springboot -n financial
+kubectl apply -f frontend-springboot-deployment.yaml -n financial
diff --git a/financial/ai-services/env.properties b/financial/ai-services/env.properties
new file mode 100644
index 00000000..2406411c
--- /dev/null
+++ b/financial/ai-services/env.properties
@@ -0,0 +1,10 @@
+OCICONFIG_FILE=~/.oci/config
+OCICONFIG_PROFILE=DEFAULT
+COMPARTMENT_ID=ocid1.compartment.oc1..mycompartmentvalue
+OBJECTSTORAGE_NAMESPACE=myobjectstorenamespacename
+OBJECTSTORAGE_BUCKETNAME=myobjectstorebucketname
+ORDS_ENDPOINT_URL=https://myordsendpointurl
+OCI_VISION_SERVICE_ENDPOINT=https://vision.aiservice.myregion.oci.oraclecloud.com
+OCI_SPEECH_SERVICE_ENDPOINT=https://speech.aiservice.myregion.oci.oraclecloud.com
+OCI_GENAI_SERVICE_ENDPOINT=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
+OPENAI_KEY=mykeyfordalleifused
diff --git a/financial/ai-services/frontend-springboot-deployment.yaml b/financial/ai-services/frontend-springboot-deployment.yaml
new file mode 100644
index 00000000..28721536
--- /dev/null
+++ b/financial/ai-services/frontend-springboot-deployment.yaml
@@ -0,0 +1,64 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: frontend-springboot
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: frontend
+ template:
+ metadata:
+ labels:
+ app: frontend
+ version: springboot
+ spec:
+ containers:
+ - name: frontend
+ image: us-ashburn-1.ocir.io/oradbclouducm/financial/frontend:0.3
+ imagePullPolicy: Always
+ env:
+ - name: LOG_LEVEL
+ value: "DEBUG"
+ - name: server_port
+ value: "8080"
+ - name: db_user
+ value: "inventoryuser"
+ - name: spring.datasource.username
+ value: "inventoryuser"
+ - name: db_url
+ value: "jdbc:oracle:thin:@${frontend_DB_ALIAS}?TNS_ADMIN=/msdataworkshop/creds"
+ - name: spring.datasource.url
+ value: "jdbc:oracle:thin:@${frontend_DB_ALIAS}?TNS_ADMIN=/msdataworkshop/creds"
+ - name: db_queueOwner
+ value: "AQ"
+ - name: db_orderQueueName
+ value: "orderqueue"
+ - name: db_frontendQueueName
+ value: "frontendqueue"
+ - name: OCI_REGION
+ value: "${OCI_REGION-}"
+ - name: VAULT_SECRET_OCID
+ value: "${VAULT_SECRET_OCID-}"
+ - name: db_password
+ valueFrom:
+ secretKeyRef:
+ name: dbuser
+ key: dbpassword
+ optional: true #not needed/used if using VAULT_SECRET_OCID exists
+ - name: spring.datasource.password
+ valueFrom:
+ secretKeyRef:
+ name: dbuser
+ key: dbpassword
+ optional: true #not needed/used if using VAULT_SECRET_OCID exists
+ volumeMounts:
+ - name: creds
+ mountPath: /msdataworkshop/creds
+ ports:
+ - containerPort: 8080
+ restartPolicy: Always
+ volumes:
+ - name: creds
+ secret:
+ secretName: frontend-db-tns-admin-secret
diff --git a/financial/ai-services/frontend-springboot-service.yaml b/financial/ai-services/frontend-springboot-service.yaml
new file mode 100644
index 00000000..5098cc8c
--- /dev/null
+++ b/financial/ai-services/frontend-springboot-service.yaml
@@ -0,0 +1,17 @@
+
+##
+## Copyright (c) 2021 Oracle and/or its affiliates.
+## Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
+apiVersion: v1
+kind: Service
+metadata:
+ name: frontend
+ labels:
+ app: frontend
+spec:
+ type: NodePort
+ ports:
+ - port: 8080
+ name: http
+ selector:
+ app: frontend
diff --git a/financial/ai-services/k8s/createSecretFromWallet.sh b/financial/ai-services/k8s/createSecretFromWallet.sh
new file mode 100755
index 00000000..26cee254
--- /dev/null
+++ b/financial/ai-services/k8s/createSecretFromWallet.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#simply copy this file to and run it from your wallet dir...
+if kubectl apply -f - ; then
+ echo "secret applied for wallet."
+else
+ echo "Error: Failure to create ragdb-wallet-secret."
+fi <
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.1
+
+
+ oracleai
+ oracleai
+ 0.0.1-SNAPSHOT
+ oracleai
+ Oracle AI Demos
+
+
+ 3.52.1
+ 21.7.0.0
+
+
+
+
+
+
+ com.google.cloud
+ libraries-bom
+ 26.32.0
+ pom
+ import
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.json
+ json
+ 20231013
+
+
+ commons-io
+ commons-io
+ 2.8.0
+
+
+ com.oracle.cloud.spring
+ spring-cloud-oci-starter
+ 1.4.0
+
+
+ com.oracle.oci.sdk
+ oci-java-sdk-common-httpclient-jersey
+ ${oci.sdk.version}
+
+
+ com.oracle.oci.sdk
+ oci-java-sdk-generativeaiinference
+ ${oci.sdk.version}
+
+
+ com.oracle.oci.sdk
+ oci-java-sdk-aivision
+ ${oci.sdk.version}
+
+
+ com.oracle.oci.sdk
+ oci-java-sdk-aispeech
+ ${oci.sdk.version}
+
+
+ com.oracle.oci.sdk
+ oci-java-sdk-ailanguage
+ ${oci.sdk.version}
+
+
+ com.oracle.oci.sdk
+ oci-java-sdk-objectstorage
+ ${oci.sdk.version}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ dev.langchain4j
+ langchain4j-oracle
+ 1.0.0-beta2
+
+
+
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.6
+
+
+ javax.xml.bind
+ jaxb-api
+ 2.4.0-b180830.0359
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ com.theokanning.openai-gpt3-java
+ service
+ 0.12.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+
+
+
+ com.oracle.database.jdbc
+ ojdbc8
+ ${oracle.jdbc.version}
+
+
+ com.oracle.database.jdbc
+ ucp
+ ${oracle.jdbc.version}
+
+
+ com.oracle.database.security
+ oraclepki
+ ${oracle.jdbc.version}
+
+
+ com.oracle.database.security
+ osdt_core
+ ${oracle.jdbc.version}
+
+
+ com.oracle.database.security
+ osdt_cert
+ ${oracle.jdbc.version}
+
+
+
+ com.google.cloud
+ google-cloud-texttospeech
+
+
+ com.google.cloud
+ google-cloud-speech
+
+
+ net.sourceforge.argparse4j
+ argparse4j
+ 0.9.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-websocket
+
+
+
+
+ commons-cli
+ commons-cli
+ 1.6.0
+
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ 1.18.0
+
+
+
+ jakarta.websocket
+ jakarta.websocket-api
+ 2.2.0
+
+
+
+
+ org.apache.tomcat.embed
+ tomcat-embed-websocket
+
+
+
+
+ org.glassfish.tyrus
+ tyrus-server
+ 2.1.3
+
+
+
+ org.glassfish.tyrus
+ tyrus-container-servlet
+ 2.1.3
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 1111
+
+
+
+
+
+
diff --git a/financial/ai-services/src/main/java/oracleai/AIApplication.java b/financial/ai-services/src/main/java/oracleai/AIApplication.java
new file mode 100644
index 00000000..96ce305e
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/AIApplication.java
@@ -0,0 +1,37 @@
+package oracleai;
+
+import com.oracle.bmc.retrier.RetryConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+@SpringBootApplication
+public class AIApplication {
+
+ public static final String COMPARTMENT_ID = System.getenv("COMPARTMENT_ID");
+ public static final String OBJECTSTORAGE_NAMESPACE = System.getenv("OBJECTSTORAGE_NAMESPACE");
+ public static final String OBJECTSTORAGE_BUCKETNAME = System.getenv("OBJECTSTORAGE_BUCKETNAME");
+ public static final String ORDS_ENDPOINT_URL = System.getenv("ORDS_ENDPOINT_URL");
+ public static final String ORDS_OMLOPSENDPOINT_URL= System.getenv("ORDS_ENDPOINT_URL") + "/omlopsuser/";
+ public static final String OCI_VISION_SERVICE_ENDPOINT = System.getenv("OCI_VISION_SERVICE_ENDPOINT");
+ public static final String OCICONFIG_FILE = System.getenv("OCICONFIG_FILE");
+ public static final String OCICONFIG_PROFILE = System.getenv("OCICONFIG_PROFILE");
+ public static final String DIGITAL_DOUBLES_IMAGES_ENDPOINT = System.getenv("DIGITAL_DOUBLES_IMAGES_ENDPOINT");
+ public static final String THREEDEY = "msy_mykey";
+
+ static {
+ System.out.println("AIApplication.static initializer COMPARTMENT_ID:" + COMPARTMENT_ID);
+ System.out.println("AIApplication.static initializer OBJECTSTORAGE_NAMESPACE:" + OBJECTSTORAGE_NAMESPACE);
+ System.out.println("AIApplication.static initializer OBJECTSTORAGE_BUCKETNAME:" + OBJECTSTORAGE_BUCKETNAME);
+ System.out.println("AIApplication.static initializer ORDS_ENDPOINT_URL:" + ORDS_ENDPOINT_URL);
+ System.out.println("AIApplication.static initializer OCI_VISION_SERVICE_ENDPOINT:" + OCI_VISION_SERVICE_ENDPOINT);
+ }
+
+ public static void main(String[] args) {
+// RetryConfiguration retryConfiguration = RetryConfiguration.builder()
+// .terminationStrategy(RetryUtils.createExponentialBackoffStrategy(500, 5)) // Configure limits
+// .build();
+ SpringApplication.run(AIApplication.class, args);
+ }
+
+}
diff --git a/financial/ai-services/src/main/java/oracleai/BinaryServerConfigurator.java b/financial/ai-services/src/main/java/oracleai/BinaryServerConfigurator.java
new file mode 100644
index 00000000..affa5503
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/BinaryServerConfigurator.java
@@ -0,0 +1,11 @@
+package oracleai;
+
+import jakarta.websocket.server.ServerEndpointConfig;
+
+public class BinaryServerConfigurator extends ServerEndpointConfig.Configurator {
+ @Override
+ public boolean checkOrigin(String originHeaderValue) {
+ System.out.println("✅ WebSocket checkOrigin originHeaderValue: " + originHeaderValue);
+ return true; // Allow all origins for WebSocket
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/ExplainAndAdviseOnHealthTestResults.java b/financial/ai-services/src/main/java/oracleai/ExplainAndAdviseOnHealthTestResults.java
new file mode 100644
index 00000000..f07506f1
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/ExplainAndAdviseOnHealthTestResults.java
@@ -0,0 +1,69 @@
+package oracleai;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.oracle.bmc.aivision.model.ImageTextDetectionFeature;
+import com.oracle.bmc.generativeaiinference.model.OnDemandServingMode;
+import oracleai.services.ORDSCalls;
+import oracleai.services.OracleGenAI;
+import oracleai.services.OracleObjectStore;
+import oracleai.services.OracleVisionAI;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import org.springframework.ui.Model;
+
+@Controller
+@RequestMapping("/health")
+public class ExplainAndAdviseOnHealthTestResults {
+
+
+ @PostMapping("/analyzedoc")
+ public String analyzedoc(@RequestParam("file") MultipartFile multipartFile,
+ @RequestParam("opts") String opts, Model model)
+ throws Exception {
+ System.out.println("analyzedocmultipartFile = " + multipartFile + ", opts = " + opts);
+ String concatenatedText;
+ if (opts.equals("inline")) {
+ String objectDetectionResults = OracleVisionAI.processImage(
+ multipartFile.getBytes(), ImageTextDetectionFeature.builder().build());
+ OracleVisionAI.ImageData imageData =
+ new ObjectMapper().readValue(objectDetectionResults, OracleVisionAI.ImageData.class);
+ concatenatedText = concatenateText(imageData);
+ } else {
+ OracleObjectStore.sendToObjectStorage(multipartFile.getOriginalFilename(), multipartFile.getInputStream());
+ concatenatedText = ORDSCalls.analyzeImageInObjectStore(
+ AIApplication.ORDS_ENDPOINT_URL + "VISIONAI_TEXTDETECTION/",
+ AIApplication.OCI_VISION_SERVICE_ENDPOINT,
+ AIApplication.COMPARTMENT_ID,
+ AIApplication.OBJECTSTORAGE_BUCKETNAME,
+ AIApplication.OBJECTSTORAGE_NAMESPACE,
+ multipartFile.getOriginalFilename(), //"objectdetectiontestimage.jpg"
+ "TEXT_DETECTION",
+ "MedicalReportSummary");
+ }
+ System.out.println(concatenatedText);
+ System.out.println("analyzedoc fullText = " + concatenatedText);
+ OnDemandServingMode chatServingMode = OnDemandServingMode.builder()
+ .modelId("cohere.command-r-16k")
+ .build();
+ String explanationOfResults =
+ OracleGenAI.builder().compartment(AIApplication.COMPARTMENT_ID)
+ .servingMode(chatServingMode)
+ .build().chat("explain these test results in simple terms, in less than 100 words, " +
+ "and tell me what should I do to get better results: \"" + concatenatedText + "\"");
+ System.out.println("ExplainAndAdviseOnHealthTestResults.analyzedoc explanationOfResults:" + explanationOfResults);
+ model.addAttribute("results", "SUMMARY WITH ADVICE: " + explanationOfResults +
+ " ...This is of course not a substitute for actual medical advice from a professional.");
+ return "resultspage";
+ }
+
+ private static String concatenateText(OracleVisionAI.ImageData imageData) {
+ if (imageData.getImageText() == null || imageData.getImageText().getWords() == null) return "";
+ StringBuilder sb = new StringBuilder();
+ for (OracleVisionAI.Word word : imageData.getImageText().getWords()) {
+ sb.append(word.getText()).append(" ");
+ }
+ return sb.toString().trim();
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/FaceRecognition.java b/financial/ai-services/src/main/java/oracleai/FaceRecognition.java
new file mode 100644
index 00000000..3bc61f05
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/FaceRecognition.java
@@ -0,0 +1,87 @@
+package oracleai;
+
+import com.oracle.bmc.aivision.model.FaceDetectionFeature;
+import oracleai.services.OracleVisionAI;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+
+@Controller
+@RequestMapping("/facerecognition")
+public class FaceRecognition {
+
+
+ @PostMapping("/facerecognition")
+ public String facerecognition(@RequestParam("file") MultipartFile multipartFile, Model model)
+ throws Exception {
+ model.addAttribute("results",
+ OracleVisionAI.processImage(multipartFile.getBytes(), FaceDetectionFeature.builder().shouldReturnLandmarks(true).build()));
+ return "resultspage";
+ }
+ }
+
+/**
+ * {
+ * "ontologyClasses": [],
+ * "detectedFaces": [
+ * {
+ * "confidence": 0.9453162,
+ * "boundingPolygon": {
+ * "normalizedVertices": [
+ * {
+ * "x": 0.43885306576845223,
+ * "y": 0.33600531005859374
+ * },
+ * {
+ * "x": 0.5433995575670001,
+ * "y": 0.33600531005859374
+ * },
+ * {
+ * "x": 0.5433995575670001,
+ * "y": 0.404624267578125
+ * },
+ * {
+ * "x": 0.43885306576845223,
+ * "y": 0.404624267578125
+ * }
+ * ]
+ * },
+ * "qualityScore": 0.887661,
+ * "landmarks": [
+ * {
+ * "type": "LEFT_EYE",
+ * "x": 0.46573874,
+ * "y": 0.36125
+ * },
+ * {
+ * "type": "RIGHT_EYE",
+ * "x": 0.5149893,
+ * "y": 0.36175
+ * },
+ * {
+ * "type": "NOSE_TIP",
+ * "x": 0.4898287,
+ * "y": 0.37575
+ * },
+ * {
+ * "type": "LEFT_EDGE_OF_MOUTH",
+ * "x": 0.46734476,
+ * "y": 0.3845
+ * },
+ * {
+ * "type": "RIGHT_EDGE_OF_MOUTH",
+ * "x": 0.51338327,
+ * "y": 0.38475
+ * }
+ * ]
+ * }
+ * ],
+ * "faceDetectionModelVersion": "1.0.29",
+ * "errors": []
+ * }
+ */
+
diff --git a/financial/ai-services/src/main/java/oracleai/GenerateAPictureStoryUsingOnlySpeech.java b/financial/ai-services/src/main/java/oracleai/GenerateAPictureStoryUsingOnlySpeech.java
new file mode 100644
index 00000000..47f2fddb
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/GenerateAPictureStoryUsingOnlySpeech.java
@@ -0,0 +1,189 @@
+package oracleai;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import oracleai.services.ImageGeneration;
+import oracleai.services.OracleObjectStore;
+import oracleai.services.OracleSpeechAI;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.sound.sampled.*;
+import java.io.*;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Controller
+@RequestMapping("/picturestory")
+public class GenerateAPictureStoryUsingOnlySpeech {
+
+ static List imageLocations = new ArrayList();
+
+ @GetMapping("/reset")
+ public String reset(Model model) {
+ imageLocations = new ArrayList();
+ model.addAttribute("results", "story board cleared successfully");
+ return "resultspage";
+ }
+
+ @PostMapping("/picturestory")
+ public String picturestory(@RequestParam("opts") String opts,
+ @RequestParam("genopts") String genopts,
+ @RequestParam("file") MultipartFile multipartFile,
+ Model model) throws Exception {
+ if (opts.equals("fileaudio") ) return fileaudio(genopts, multipartFile, model);
+ else return liveaudio(genopts, model);
+ }
+
+ @NotNull
+ private String fileaudio(String genopts, MultipartFile multipartFile, Model model) throws Exception {
+ OracleObjectStore.sendToObjectStorage(multipartFile.getOriginalFilename(), multipartFile.getInputStream());
+ String transcriptionJobId = OracleSpeechAI.getTranscriptFromOCISpeech(multipartFile.getOriginalFilename());
+ System.out.println("transcriptionJobId: " + transcriptionJobId);
+ String jsonTranscriptFromObjectStorage =
+ OracleObjectStore.getFromObjectStorage(transcriptionJobId,
+ AIApplication.OBJECTSTORAGE_NAMESPACE + "_" +
+ AIApplication.OBJECTSTORAGE_BUCKETNAME + "_" +
+ multipartFile.getOriginalFilename() + ".json");
+ System.out.println("jsonTranscriptFromObjectStorage: " + jsonTranscriptFromObjectStorage);
+ String pictureDescription = getConcatenatedTokens(jsonTranscriptFromObjectStorage);
+ imageLocations.add(ImageGeneration.imagegeneration(pictureDescription + " " + genopts));
+ model.addAttribute("imageLocations", imageLocations.toArray(new String[0]));
+ return "resultswithimages";
+ }
+
+ public String liveaudio(String genopts, Model model) throws Exception {
+ AudioFormat format =
+ new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100.0f, 16, 1,
+ (16 / 8) * 1, 44100.0f, true);
+ SoundRecorder soundRecorder = new SoundRecorder();
+ soundRecorder.build(format);
+ System.out.println("Start recording ....");
+ soundRecorder.start();
+ Thread.sleep(8000);
+ soundRecorder.stop();
+ System.out.println("Stopped recording ....");
+ Thread.sleep(5000); //give the process time
+ String name = "AISoundClip";
+ AudioFileFormat.Type fileType = AudioFileFormat.Type.WAVE;
+ AudioInputStream audioInputStream = soundRecorder.audioInputStream;
+ System.out.println("Saving...");
+ File file = new File(name + "." + fileType.getExtension());
+ audioInputStream.reset();
+ AudioSystem.write(audioInputStream, fileType, file);
+ System.out.println("Saved " + file.getAbsolutePath());
+ OracleObjectStore.sendToObjectStorage(file.getName(), new FileInputStream(file));
+ String transcriptionJobId = OracleSpeechAI.getTranscriptFromOCISpeech(file.getName());
+ System.out.println("transcriptionJobId: " + transcriptionJobId);
+ String jsonTranscriptFromObjectStorage =
+ OracleObjectStore.getFromObjectStorage(transcriptionJobId,
+ AIApplication.OBJECTSTORAGE_NAMESPACE + "_" +
+ AIApplication.OBJECTSTORAGE_BUCKETNAME + "_" + file.getName() + ".json");
+ System.out.println("jsonTranscriptFromObjectStorage: " + jsonTranscriptFromObjectStorage);
+ String pictureDescription = getConcatenatedTokens(jsonTranscriptFromObjectStorage);
+ imageLocations.add(ImageGeneration.imagegeneration(pictureDescription + " " + genopts));
+ model.addAttribute("imageLocations", imageLocations.toArray(new String[0]));
+ return "resultswithimages";
+ }
+
+ public String getConcatenatedTokens(String json) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ try {
+ OracleSpeechAI.TranscriptionResponse response =
+ objectMapper.readValue(json, OracleSpeechAI.TranscriptionResponse.class);
+ return response.getTranscriptions().stream()
+ .flatMap(transcription -> transcription.getTokens().stream())
+ .map(OracleSpeechAI.TranscriptionResponse.Transcription.Token::getToken)
+ .collect(Collectors.joining(" "));
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public class SoundRecorder implements Runnable {
+ AudioInputStream audioInputStream;
+ private AudioFormat format;
+ public Thread thread;
+
+
+ public SoundRecorder build(AudioFormat format) {
+ this.format = format;
+ return this;
+ }
+
+ public void start() {
+ thread = new Thread(this);
+ thread.start();
+ }
+
+ public void stop() {
+ thread = null;
+ }
+
+ @Override
+ public void run() {
+ try (final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final TargetDataLine line = getTargetDataLineForRecord();) {
+ int frameSizeInBytes = format.getFrameSize();
+ int bufferLengthInFrames = line.getBufferSize() / 8;
+ final int bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes;
+ buildByteOutputStream(out, line, frameSizeInBytes, bufferLengthInBytes);
+ this.audioInputStream = new AudioInputStream(line);
+ setAudioInputStream(convertToAudioIStream(out, frameSizeInBytes));
+ audioInputStream.reset();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ public void buildByteOutputStream(final ByteArrayOutputStream out,
+ final TargetDataLine line, int frameSizeInBytes,
+ final int bufferLengthInBytes) throws IOException {
+ final byte[] data = new byte[bufferLengthInBytes];
+ int numBytesRead;
+
+ line.start();
+ while (thread != null) {
+ if ((numBytesRead = line.read(data, 0, bufferLengthInBytes)) == -1) {
+ break;
+ }
+ out.write(data, 0, numBytesRead);
+ }
+ }
+
+ private void setAudioInputStream(AudioInputStream aStream) {
+ this.audioInputStream = aStream;
+ }
+
+ public AudioInputStream convertToAudioIStream(final ByteArrayOutputStream out, int frameSizeInBytes) {
+ byte[] audioBytes = out.toByteArray();
+ AudioInputStream audioStream =
+ new AudioInputStream(new ByteArrayInputStream(audioBytes), format,
+ audioBytes.length / frameSizeInBytes);
+ System.out.println("Recording finished");
+ return audioStream;
+ }
+
+ public TargetDataLine getTargetDataLineForRecord() {
+ TargetDataLine line;
+ DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
+ if (!AudioSystem.isLineSupported(info)) {
+ throw new UnsupportedOperationException("Line not supported");
+ }
+ try {
+ line = (TargetDataLine) AudioSystem.getLine(info);
+ line.open(format, line.getBufferSize());
+ } catch (final Exception ex) {
+ return null;
+ }
+ return line;
+ }
+ }
+
+}
diff --git a/financial/ai-services/src/main/java/oracleai/OracleVectorStoreBean.java b/financial/ai-services/src/main/java/oracleai/OracleVectorStoreBean.java
new file mode 100644
index 00000000..447b569a
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/OracleVectorStoreBean.java
@@ -0,0 +1,23 @@
+package oracleai;
+
+//import org.springframework.ai.embedding.EmbeddingModel;
+//import org.springframework.ai.vectorstore.VectorStore;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.jdbc.core.JdbcTemplate;
+//import org.springframework.ai.vectorstore.oracle.OracleVectorStore;
+//import org.springframework.ai.vectorstore.oracle.OracleVectorStore.*;
+
+public class OracleVectorStoreBean {
+
+// @Bean
+// public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
+// return OracleVectorStore.builder(jdbcTemplate, embeddingModel)
+// .tableName("my_vectors")
+// .indexType(OracleVectorStoreIndexType.IVF)
+// .distanceType(OracleVectorStoreDistanceType.COSINE)
+// .dimensions(1536)
+// .searchAccuracy(95)
+// .initializeSchema(true)
+// .build();
+// }
+}
\ No newline at end of file
diff --git a/financial/ai-services/src/main/java/oracleai/SelectAI_NL2SQL.java b/financial/ai-services/src/main/java/oracleai/SelectAI_NL2SQL.java
new file mode 100644
index 00000000..5f68784f
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/SelectAI_NL2SQL.java
@@ -0,0 +1,28 @@
+package oracleai;
+
+import com.oracle.bmc.aivision.model.FaceDetectionFeature;
+import oracleai.services.OracleVisionAI;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+@Controller
+@RequestMapping("/selectai")
+public class SelectAI_NL2SQL {
+
+//@Autowired
+//javax.sql.DataSource dataSource;
+
+ @PostMapping("/test")
+ public String textsearch(Model model) throws Exception{
+// System.out.println("SelectAI_NL2SQL.test dataSource:" + dataSource);
+// model.addAttribute("results", dataSource.getConnection());
+ return "resultspage";
+ }
+
+}
\ No newline at end of file
diff --git a/financial/ai-services/src/main/java/oracleai/SpeechWebSocketConfigurator.java b/financial/ai-services/src/main/java/oracleai/SpeechWebSocketConfigurator.java
new file mode 100644
index 00000000..47ced3bc
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/SpeechWebSocketConfigurator.java
@@ -0,0 +1,12 @@
+package oracleai;
+
+import jakarta.websocket.server.HandshakeRequest;
+import jakarta.websocket.HandshakeResponse;
+import jakarta.websocket.server.ServerEndpointConfig;
+
+public class SpeechWebSocketConfigurator extends ServerEndpointConfig.Configurator {
+ @Override
+ public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+ sec.getUserProperties().put("org.apache.tomcat.websocket.binaryBufferSize", 1024 * 1024); // Enable binary message support
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/SpeechWebSocketServer.java0 b/financial/ai-services/src/main/java/oracleai/SpeechWebSocketServer.java0
new file mode 100644
index 00000000..6769a334
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/SpeechWebSocketServer.java0
@@ -0,0 +1,160 @@
+package oracleai;
+
+import com.google.api.gax.rpc.BidiStreamingCallable;
+import com.google.api.gax.rpc.ApiStreamObserver;
+import com.google.cloud.speech.v1.*;
+import com.google.protobuf.ByteString;
+
+import jakarta.websocket.*;
+import jakarta.websocket.server.ServerEndpoint;
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import javax.sound.sampled.*;
+
+import org.springframework.stereotype.Component;
+
+@ServerEndpoint(value = "/speech", configurator = SpeechWebSocketConfigurator.class)
+@Component
+public class SpeechWebSocketServer {
+ private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
+ private static SpeechClient speechClient;
+ private ApiStreamObserver requestObserver;
+
+ static {
+ try {
+ speechClient = SpeechClient.create();
+ } catch (IOException e) {
+ throw new RuntimeException("❌ Failed to initialize SpeechClient", e);
+ }
+ }
+
+ @OnOpen
+ public void onOpen(Session session) {
+ System.out.println("✅ WebSocket Connected: " + session.getId());
+ session.setMaxBinaryMessageBufferSize(1024 * 1024); // Allow large audio messages
+
+ ApiStreamObserver responseObserver = new ApiStreamObserver<>() {
+ @Override
+ public void onNext(StreamingRecognizeResponse response) {
+ for (StreamingRecognitionResult result : response.getResultsList()) {
+ if (result.getAlternativesCount() > 0) {
+ String transcript = result.getAlternatives(0).getTranscript().trim();
+ if (!transcript.isEmpty()) {
+ System.out.println("📝 Transcription: " + transcript);
+ try {
+ session.getBasicRemote().sendText(transcript);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ System.err.println("❌ Google API Error: " + t.getMessage());
+ }
+
+ @Override
+ public void onCompleted() {
+ System.out.println("✅ Streaming completed.");
+ }
+ };
+
+ // Initialize Streaming to Google Speech API
+ BidiStreamingCallable callable =
+ speechClient.streamingRecognizeCallable();
+ requestObserver = callable.bidiStreamingCall(responseObserver);
+
+ requestObserver.onNext(StreamingRecognizeRequest.newBuilder()
+ .setStreamingConfig(StreamingRecognitionConfig.newBuilder()
+ .setConfig(RecognitionConfig.newBuilder()
+ .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
+ .setSampleRateHertz(16000)
+ .setLanguageCode("en-US")
+ .setAudioChannelCount(1)
+ .setEnableAutomaticPunctuation(true)
+ .build())
+ .setInterimResults(true)
+ .setSingleUtterance(false)
+ .build())
+ .build());
+ }
+
+ /**
+ * 🔹 **Handles Incoming Binary Audio Data (From WebSocket)**
+ * This method now **reads a WAV file** instead of processing real-time audio streaming.
+ */
+ @OnMessage
+ public void onMessage(Session session) {
+ String filePath = "C:/Users/opc/Downloads/audio_logs/sample.wav"; // Change to your WAV file path
+ byte[] audioBytes;
+
+ try {
+ audioBytes = readWavFile(filePath);
+ if (audioBytes == null || audioBytes.length == 0) {
+ System.out.println("⚠️ WAV file is empty or could not be read.");
+ return;
+ }
+ } catch (IOException | UnsupportedAudioFileException e) {
+ System.err.println("❌ Error reading WAV file: " + e.getMessage());
+ return;
+ }
+
+ System.out.println("✅ Sending Audio Data from WAV file: " + audioBytes.length + " bytes");
+
+ if (requestObserver != null) {
+ requestObserver.onNext(StreamingRecognizeRequest.newBuilder()
+ .setAudioContent(ByteString.copyFrom(audioBytes))
+ .build());
+ }
+ }
+
+ @OnClose
+ public void onClose(Session session) {
+ System.out.println("🔴 WebSocket Closed: " + session.getId());
+ if (requestObserver != null) {
+ requestObserver.onCompleted();
+ }
+ }
+
+ @OnError
+ public void onError(Session session, Throwable throwable) {
+ System.err.println("⚠️ WebSocket error: " + throwable.getMessage());
+ }
+
+ /**
+ * **🔹 Reads WAV File and Extracts PCM Data**
+ * - Converts **WAV file** to **raw PCM data**.
+ * - Ensures it is in the **correct format** (16-bit mono PCM).
+ */
+ private byte[] readWavFile(String filePath) throws IOException, UnsupportedAudioFileException {
+ File file = new File(filePath);
+ AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);
+ AudioFormat format = audioInputStream.getFormat();
+
+ System.out.println("🎵 WAV File Format: " + format);
+
+ // Convert to PCM Signed if necessary
+ if (format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
+ AudioFormat pcmFormat = new AudioFormat(
+ AudioFormat.Encoding.PCM_SIGNED,
+ format.getSampleRate(),
+ 16, // Force 16-bit audio
+ format.getChannels(),
+ format.getChannels() * 2,
+ format.getSampleRate(),
+ false // Little-endian
+ );
+ audioInputStream = AudioSystem.getAudioInputStream(pcmFormat, audioInputStream);
+ }
+
+ // Read raw audio data
+ byte[] audioBytes = audioInputStream.readAllBytes();
+ audioInputStream.close();
+ return audioBytes;
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/TextSearch.java b/financial/ai-services/src/main/java/oracleai/TextSearch.java
new file mode 100644
index 00000000..1b648980
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/TextSearch.java
@@ -0,0 +1,24 @@
+package oracleai;
+
+
+import oracleai.services.ORDSCalls;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+
+@Controller
+@RequestMapping("/textsearch")
+public class TextSearch {
+
+ @PostMapping("/textsearch")
+ public String textsearch(@RequestParam("sql") String sql, Model model) {
+ String explanationOfResults = ORDSCalls.executeTextSearchContains(
+ AIApplication.ORDS_ENDPOINT_URL + "VISIONAI_RESULTS_TEXT_SEARCH/", sql);
+ model.addAttribute("results", explanationOfResults);
+ return "resultspage";
+ }
+
+}
diff --git a/financial/ai-services/src/main/java/oracleai/UploadDownloadImage.java b/financial/ai-services/src/main/java/oracleai/UploadDownloadImage.java
new file mode 100644
index 00000000..fd566a89
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/UploadDownloadImage.java
@@ -0,0 +1,30 @@
+package oracleai;
+
+import oracleai.digitaldouble.ImageStore;
+import oracleai.services.ORDSCalls;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+
+@Controller
+@RequestMapping("/transferimage")
+public class UploadDownloadImage {
+
+ @PostMapping("/uploadimage")
+ public String uploadImage(@RequestParam("image") MultipartFile image, Model model) {
+ ORDSCalls.uploadImage(image);
+ System.out.println("Image upload complete for: " + image.getOriginalFilename());
+ ImageStore[] imageStores = ORDSCalls.getImageStoreData();
+ model.addAttribute("images", imageStores);
+ return "images";
+ }
+
+ @GetMapping("/downloadimages")
+ public String getImageStoreData(Model model) {
+ ImageStore[] imageStores = ORDSCalls.getImageStoreData();
+ model.addAttribute("images", imageStores);
+ return "images";
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/WebConfig.java b/financial/ai-services/src/main/java/oracleai/WebConfig.java
new file mode 100644
index 00000000..e9d1c5f0
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/WebConfig.java
@@ -0,0 +1,23 @@
+package oracleai;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig {
+ @Bean
+ public WebMvcConfigurer corsConfigurer() {
+ return new WebMvcConfigurer() {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**") // Apply to all endpoints
+ .allowedOriginPatterns("*") // ✅ Use allowedOriginPatterns instead of "*"
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedHeaders("*")
+ .allowCredentials(true); // ✅ Keep credentials enabled
+ }
+ };
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/WriteAStoryAboutAPictureAndGiveItsSentiments.java b/financial/ai-services/src/main/java/oracleai/WriteAStoryAboutAPictureAndGiveItsSentiments.java
new file mode 100644
index 00000000..7e15d6c5
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/WriteAStoryAboutAPictureAndGiveItsSentiments.java
@@ -0,0 +1,53 @@
+package oracleai;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.oracle.bmc.aivision.model.*;
+import oracleai.services.*;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.ui.Model;
+
+@Controller
+@RequestMapping("/tellastory")
+public class WriteAStoryAboutAPictureAndGiveItsSentiments {
+
+ @PostMapping("/tellastory")
+ public String tellastory(@RequestParam("file") MultipartFile multipartFile, @RequestParam("opts") String opts,
+ @RequestParam("genopts") String genopts, Model model)
+ throws Exception {
+ System.out.println("WriteAStoryAboutAPictureAndGiveItsSentiments.tellastory file = " +
+ multipartFile.getOriginalFilename());
+ String fullText = "";
+ if(opts.equals("inline")) {
+ String objectDetectionResults =
+ OracleVisionAI.processImage(multipartFile.getBytes(),
+ ImageObjectDetectionFeature.builder().maxResults(10).build());
+ OracleVisionAI.ImageAnalysisResult imageAnalysis =
+ new ObjectMapper().readValue(objectDetectionResults, OracleVisionAI.ImageAnalysisResult.class);
+ for (OracleVisionAI.ImageObject image : imageAnalysis.getImageObjects()) fullText += image.getName() + ", ";
+ System.out.println("WriteAStoryAboutAPictureAndGiveItsSentiments.tellastory images = " + fullText);
+ }
+ else {
+ OracleObjectStore.sendToObjectStorage(multipartFile.getOriginalFilename(), multipartFile.getInputStream());
+ fullText = ORDSCalls.analyzeImageInObjectStore(
+ AIApplication.ORDS_ENDPOINT_URL + "VISIONAI_OBJECTDETECTION/",
+ AIApplication.OCI_VISION_SERVICE_ENDPOINT,
+ AIApplication.COMPARTMENT_ID,
+ AIApplication.OBJECTSTORAGE_BUCKETNAME,
+ AIApplication.OBJECTSTORAGE_NAMESPACE,
+ multipartFile.getOriginalFilename(), //"objectdetectiontestimage.jpg"
+ "OBJECT_DETECTION",
+ "TellAStory");
+ }
+ String generatedstory = OracleGenAI.builder().build().chat("using strong negative and positive sentiments, " +
+ "write a story that is " + genopts + " and includes " + fullText );
+ model.addAttribute("results", "STORY: " + generatedstory +
+ " --->SENTIMENT ANALYSIS: " + OracleLanguageAI.sentimentAnalysis(generatedstory) );
+ return "resultspage";
+ }
+
+}
+
+
+
diff --git a/financial/ai-services/src/main/java/oracleai/aiholo/AIHoloController.java b/financial/ai-services/src/main/java/oracleai/aiholo/AIHoloController.java
new file mode 100644
index 00000000..c64cb13b
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/aiholo/AIHoloController.java
@@ -0,0 +1,421 @@
+package oracleai.aiholo;
+
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
+import dev.langchain4j.store.embedding.EmbeddingSearchResult;
+import dev.langchain4j.store.embedding.oracle.CreateOption;
+import dev.langchain4j.store.embedding.oracle.EmbeddingTable;
+import dev.langchain4j.store.embedding.oracle.Index;
+import dev.langchain4j.store.embedding.oracle.OracleEmbeddingStore;
+import org.json.JSONObject;
+//import org.springframework.ai.document.Document;
+//import org.springframework.ai.vectorstore.SearchRequest;
+//import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+import com.google.cloud.texttospeech.v1.AudioEncoding;
+import com.google.cloud.texttospeech.v1.SsmlVoiceGender;
+import com.google.cloud.texttospeech.v1.SynthesisInput;
+import com.google.cloud.texttospeech.v1.SynthesizeSpeechResponse;
+import com.google.cloud.texttospeech.v1.TextToSpeechClient;
+import com.google.cloud.texttospeech.v1.VoiceSelectionParams;
+import com.google.protobuf.ByteString;
+import com.google.cloud.texttospeech.v1.AudioConfig;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.sql.*;
+
+import java.sql.*;
+import java.util.HashMap;
+import java.util.Map;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+@Controller
+@RequestMapping("/aiholo")
+// @CrossOrigin(origins = "*")
+public class AIHoloController {
+ private String theValue = "mirrorme";
+ private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+ private static final String SANDBOX_API_URL = System.getenv("SANDBOX_API_URL");
+ private static final String SANDBOX_AUTH_TOKEN = System.getenv("SANDBOX_AUTH_TOKEN");
+ static final String AUDIO_DIR_PATH = System.getenv("AUDIO_DIR_PATH");
+ private static final String DEFAULT_LANGUAGE_CODE = "es-ES";
+ private static final String DEFAULT_VOICE_NAME = "es-ES-Wavenet-D";
+ private final static String sql = """
+ SELECT DBMS_CLOUD_AI.GENERATE(
+ prompt => ?,
+ profile_name => 'VIDEOGAMES_PROFILE',
+ action => ?
+ ) FROM dual
+ """;
+
+ @Autowired
+ private DataSource dataSource;
+
+ private static final Object metahumanLock = new Object();
+ private static boolean isRecentQuestionProcessed;
+ private static String languageCode = "es";
+
+ public AIHoloController() {
+// startInactivityMonitor();
+ }
+
+ private void startInactivityMonitor() {
+ System.out.println("startInactivityMonitor...");
+ scheduler.scheduleAtFixedRate(() -> {
+ if (isRecentQuestionProcessed) {
+ System.out.println("isRecentQuestionProcessed true so skipping the timecheck/keepalive");
+ isRecentQuestionProcessed = false;
+ }
+// String fileName = "currenttime.wav"; //testing123-brazil.wav
+// TTSAndAudio2Face.processMetahuman(
+// fileName, TimeInWords.getTimeInWords(languageCode),
+// DEFAULT_LANGUAGE_CODE, DEFAULT_VOICE_NAME);
+ TTSAndAudio2Face.sendToAudio2Face("../audio-aiholo/explainer.wav");
+ }, 1, 15, TimeUnit.MINUTES);
+ }
+
+
+ @GetMapping("")
+ public String home(@RequestParam(value = "languageCode", defaultValue = "en-GB") String languageCode, Model model) {
+ System.out.println("AIHolo root languageCode = " + languageCode );
+ this.languageCode = languageCode;
+ model.addAttribute("languageCode", languageCode);
+ if (languageCode.equals("pt-BR"))
+ model.addAttribute("voiceName", "pt-BR-Wavenet-D");
+ else if (languageCode.equals("es-ES"))
+ model.addAttribute("voiceName", "es-ES-Wavenet-D");
+ else if (languageCode.equals("zh-SG") )
+ model.addAttribute("voiceName", "cmn-CN-Wavenet-A");
+ else if (languageCode.equals("de-DE") )
+ model.addAttribute("voiceName", "de-DE-Wavenet-A");
+ else if (languageCode.equals("es-MX") )
+ model.addAttribute("voiceName", "es-US-Wavenet-A");
+ else if (languageCode.equals("it-IT") )
+ model.addAttribute("voiceName", "it-IT-Wavenet-A");
+ else if (languageCode.equals("ar-ae") )
+ model.addAttribute("voiceName", "ar-ae-Wavenet-A");
+ else if (languageCode.equals("ja-JP") )
+ model.addAttribute("voiceName", "ja-JP-Wavenet-A");
+ else if (languageCode.equals("hi-IN") )
+ model.addAttribute("voiceName", "hi-IN-Wavenet-A");
+ else if (languageCode.equals("en-US") || languageCode.equals("en-GB"))
+ model.addAttribute("voiceName", "en-GB-Wavenet-A");
+ else model.addAttribute("voiceName", "en-GB-Wavenet-A"); //default to GB
+ return "aiholo";
+ }
+
+
+
+ @GetMapping("/explainer")
+ @ResponseBody
+ public String explainer() throws Exception {
+ System.out.println("AIHoloController.explainer");
+ theValue = "explainer";
+ String filePath = "C:/Users/opc/aiholo_output.txt";
+ try (FileWriter writer = new FileWriter(filePath)) {
+ JSONObject json = new JSONObject();
+ json.put("data", theValue); // Store the response inside JSON
+ writer.write(json.toString());
+ writer.flush();
+ } catch (IOException e) {
+ return "Error writing to file: " + e.getMessage();
+ }
+ TTSAndAudio2Face.sendToAudio2Face("explainer.wav");
+ return "Explained";
+ }
+
+ @GetMapping("/play")
+ @ResponseBody
+ public String play(@RequestParam("question") String question,
+ @RequestParam("selectedMode") String selectedMode,
+ @RequestParam("languageCode") String languageCode,
+ @RequestParam("voiceName") String voicename) throws Exception {
+ System.out.println(
+ "play question: " + question + " selectedMode: " + selectedMode +
+ " languageCode:"+ languageCode+ " voicename:"+ voicename);
+ System.out.println("modified question: " + question );
+ theValue = "question";
+ String filePath = "C:/Users/opc/aiholo_output.txt";
+ try (FileWriter writer = new FileWriter(filePath)) {
+ JSONObject json = new JSONObject();
+ json.put("data", theValue); // Store the response inside JSON
+ writer.write(json.toString());
+ writer.flush();
+ } catch (IOException e) {
+ return "Error writing to file: " + e.getMessage();
+ }
+ String action = "chat";
+ String answer;
+ if (languageCode.equals("pt-BR")) answer = "Desculpe. Não consegui encontrar uma resposta no banco de dados";
+ else if (languageCode.equals("es-ES")) answer = "Lo siento, no pude encontrar una respuesta en la base de datos.";
+ else if (languageCode.equals("en-GB")) answer = "Sorry, I couldn't find an answer in the database.";
+ else if (languageCode.equals("zh-SG")) answer = "抱歉,我在数据库中找不到答案";
+ else answer = "I'm sorry. I couldn't find an answer in the database";
+ if (selectedMode.contains("use vector")) {
+// doesn't matter as its not select ai action = "vectorrag";
+ question = question.replace("use vectorrag", "").trim();
+ question += ". Respond in 20 words or less";
+ answer = executeSandbox(question);
+ } else {
+ if (selectedMode.contains("use narrate")) {
+ action = "narrate";
+ question = question.replace("use narrate", "").trim();
+ } else {
+ question = question.replace("use chat", "").trim();
+ }
+ question += ". Respond in 20 words or less";
+ try (Connection connection = dataSource.getConnection();
+ PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
+ System.out.println("Database Connection : " + connection);
+ String response = null;
+ preparedStatement.setString(1, question);
+ preparedStatement.setString(2, action);
+ try (ResultSet resultSet = preparedStatement.executeQuery()) {
+ if (resultSet.next()) {
+ response = resultSet.getString(1); // Retrieve AI response from the first column
+ }
+ }
+ answer = response;
+ } catch (SQLException e) {
+ System.err.println("Failed to connect to the database: " + e.getMessage());
+ return "Database Connection Failed!";
+ }
+ }
+ String fileName = "output.wav";
+ System.out.println("about to TTS and sendAudioToAudio2Face for answer: " + answer);
+ TTSAndAudio2Face.processMetahuman(fileName, answer, languageCode, voicename);
+ return answer;
+ }
+
+
+ /**
+ curl -X 'POST' \
+ 'http://host/v1/chat/completions?client=server' \
+ -H 'accept: application/json' \
+ -H 'Authorization: Bearer bearer' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "messages": [
+ {
+ "role": "user",
+ "content": "What are Alternative Dispute Resolution"
+ }
+ ]
+ }'
+ */
+
+ public String executeSandbox(String cummulativeResult) {
+ System.out.println("using AI sandbox: " + cummulativeResult);
+ Map payload = new HashMap<>();
+ Map message = new HashMap<>();
+ message.put("role", "user");
+ message.put("content", cummulativeResult);
+ payload.put("messages", new Object[] { message });
+ JSONObject jsonPayload = new JSONObject(payload);
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ headers.set("Authorization", SANDBOX_AUTH_TOKEN);
+ headers.set("Accept", "application/json");
+ headers.set("client", "server");
+ HttpEntity request = new HttpEntity<>(jsonPayload.toString(), headers);
+ RestTemplate restTemplate = new RestTemplate();
+ ResponseEntity response = restTemplate.exchange(SANDBOX_API_URL, HttpMethod.POST, request, String.class);
+ String latestAnswer;
+ if (response.getStatusCode() == HttpStatus.OK) {
+ JSONObject responseData = new JSONObject(response.getBody());
+ latestAnswer = responseData.getJSONArray("choices").getJSONObject(0).getJSONObject("message")
+ .getString("content");
+ System.out.println("RAG Full Response latest_answer: " + latestAnswer);
+ return latestAnswer;
+ } else {
+ System.out.println("Failed to fetch data: " + response.getStatusCode() + " " + response.getBody());
+ return " I'm sorry, I couldn't find an answer";
+ }
+ }
+
+
+ /**
+ * Utilites not required by Interactive AI Holograms from here to end...
+ */
+
+
+
+ // `https://host:port/aiholo/tts?textToConvert=${encodeURIComponent(textToConvert)}
+ // &languageCode=${encodeURIComponent(languageCode)}&ssmlGender=${encodeURIComponent(ssmlGender)}
+ // &voiceName=${encodeURIComponent(voiceName)}`;
+ @GetMapping("/tts")
+ public ResponseEntity ttsAndReturnAudioFile(@RequestParam("textToConvert") String textToConvert,
+ @RequestParam("languageCode") String languageCode,
+ @RequestParam("ssmlGender") String ssmlGender,
+ @RequestParam("voiceName") String voiceName) throws Exception {
+ String info= "tts for textToConvert " + textToConvert;
+ System.out.println("in TTS GCP info:" + info);
+ try (TextToSpeechClient textToSpeechClient = TextToSpeechClient.create()) {
+ System.out.println("in TTS GCP textToSpeechClient:" + textToSpeechClient + " languagecode:" + languageCode);
+ SynthesisInput input = SynthesisInput.newBuilder().setText(textToConvert).build();
+ VoiceSelectionParams voice =
+ VoiceSelectionParams.newBuilder()
+ .setLanguageCode(languageCode)
+ .setSsmlGender(SsmlVoiceGender.FEMALE) // SsmlVoiceGender.NEUTRAL SsmlVoiceGender.MALE
+ .setName(voiceName) //eg "pt-BR-Wavenet-A"
+ .build();
+ AudioConfig audioConfig =
+ AudioConfig.newBuilder()
+ .setAudioEncoding(AudioEncoding.LINEAR16) // wav AudioEncoding.MP3 being another
+ .build();
+ SynthesizeSpeechResponse response =
+ textToSpeechClient.synthesizeSpeech(input, voice, audioConfig);
+ ByteString audioContents = response.getAudioContent();
+ byte[] audioData = audioContents.toByteArray();
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.set(HttpHeaders.CONTENT_TYPE, "audio/mpeg");
+ headers.set(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"tts-" + languageCode + "" + ssmlGender+ "" + voiceName + "_" +
+ getFirst10Chars(textToConvert) + ".mp3\"");
+ return new ResponseEntity<>(audioData, headers, HttpStatus.OK);
+ }
+ }
+
+
+
+ // Vector embedding, store, langchain, etc. stuff...
+
+
+// @Autowired
+// VectorStore vectorStore;
+//
+// @GetMapping("/vectorstoretest")
+// @ResponseBody
+// public String vectorstoretest(@RequestParam("question") String question,
+// @RequestParam("selectedMode") String selectedMode,
+// @RequestParam("languageCode") String languageCode,
+// @RequestParam("voiceName") String voicename) throws Exception {
+//// System.out.println(
+// List documents = List.of(
+// new Document("Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")),
+// new Document("The World is Big and Salvation Lurks Around the Corner"),
+// new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2")));
+// // Add the documents to Oracle Vector Store
+// vectorStore.add(documents);
+// // Retrieve documents similar to a query
+// List results =
+// vectorStore.similaritySearch(SearchRequest.builder().query(question).topK(5).build());
+//// vectorStore.similaritySearch(SearchRequest.builder().query("Spring").topK(5).build());
+// return "test";
+// //results.getFirst().getFormattedContent(); give s cannot find symbol
+// //[ERROR] symbol: method getFirst()
+// //[ERROR] location: variable results of type java.util.List
+// }
+
+ @GetMapping("/langchain")
+ @ResponseBody
+ public String langchain(@RequestParam("question") String question,
+ @RequestParam("selectedMode") String selectedMode,
+ @RequestParam("languageCode") String languageCode,
+ @RequestParam("voiceName") String voicename) throws Exception {
+ EmbeddingSearchRequest embeddingSearchRequest = null;
+ OracleEmbeddingStore embeddingStore =
+ OracleEmbeddingStore.builder()
+ .dataSource(dataSource)
+ .embeddingTable(EmbeddingTable.builder()
+ .createOption(CreateOption.CREATE_OR_REPLACE)
+ .name("my_embedding_table")
+ .idColumn("id_column_name")
+ .embeddingColumn("embedding_column_name")
+ .textColumn("text_column_name")
+ .metadataColumn("metadata_column_name")
+ .build())
+ .index(Index.ivfIndexBuilder()
+ .createOption(CreateOption.CREATE_OR_REPLACE).build())
+ .build();
+ EmbeddingSearchResult embeddingSearchResult = embeddingStore.search(embeddingSearchRequest);
+ return "langchain";
+ }
+
+
+
+ //set/get etc utilites to end....
+
+
+
+
+ public static String getFirst10Chars(String textToConvert) {
+ if (textToConvert == null || textToConvert.isEmpty()) {
+ return "";
+ }
+ return textToConvert.length() > 10 ? textToConvert.substring(0, 10) : textToConvert;
+ }
+
+
+ @GetMapping("/set")
+ @ResponseBody
+ public String setValue(@RequestParam("value") String value) {
+ theValue = value;
+ System.out.println("EchoController set: " + theValue);
+ String filePath = "C:/Users/opc/aiholo_output.txt";
+ try (FileWriter writer = new FileWriter(filePath)) {
+ JSONObject json = new JSONObject();
+ json.put("data", value); // Store the response inside JSON
+ writer.write(json.toString());
+ writer.flush();
+ } catch (IOException e) {
+ return "Error writing to file: " + e.getMessage();
+ }
+
+ return "set successfully: " + theValue;
+
+ }
+
+ @GetMapping("/get")
+ @ResponseBody
+ public String getValue() {
+ System.out.println("EchoController get: " + theValue);
+ return theValue;
+ }
+
+}
+
+/**
+ en-US (American English):
+ • en-US-Neural2-F 
+ • en-US-Neural2-G 
+ • en-US-Neural2-H
+ • en-US-Neural2-I 
+ • en-US-Neural2-J 
+ • en-US-Standard-C 
+ • en-US-Standard-E
+ • en-US-Standard-G 
+ • en-US-Standard-I
+ • en-US-Wavenet-C 
+ • en-US-Wavenet-E 
+ • en-US-Wavenet-G
+ • en-US-Wavenet-I
+
+ en-GB (British English):
+ • en-GB-Neural2-C 
+ • en-GB-Neural2-E 
+ • en-GB-Standard-A 
+ • en-GB-Standard-C 
+ • en-GB-Standard-E
+ • en-GB-Wavenet-A 
+ • en-GB-Wavenet-C 
+ • en-GB-Wavenet-E
+
+ */
\ No newline at end of file
diff --git a/financial/ai-services/src/main/java/oracleai/aiholo/DataSourceConfiguration.java b/financial/ai-services/src/main/java/oracleai/aiholo/DataSourceConfiguration.java
new file mode 100644
index 00000000..5aef7ee2
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/aiholo/DataSourceConfiguration.java
@@ -0,0 +1,25 @@
+package oracleai.aiholo;
+
+import oracle.jdbc.pool.OracleDataSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+@Configuration
+public class DataSourceConfiguration {
+
+ @Bean
+ public DataSource dataSource() throws SQLException {
+ OracleDataSource dataSource = new OracleDataSource();
+ dataSource.setUser("moviestream");
+ dataSource.setPassword("Welcome12345");
+ dataSource.setURL("jdbc:oracle:thin:@selectaidb_high?TNS_ADMIN=C:/Users/opc/Downloads/Wallet_SelectAIDB");
+// try (Connection connection = dataSource.getConnection()) {
+// System.out.println("✅ Successfully connected to Oracle DB: " + connection.getMetaData().getDatabaseProductVersion());
+// }
+ return dataSource;
+ }
+}
diff --git a/financial/ai-services/src/main/java/oracleai/aiholo/TTSAndAudio2Face.java b/financial/ai-services/src/main/java/oracleai/aiholo/TTSAndAudio2Face.java
new file mode 100644
index 00000000..f50a1f28
--- /dev/null
+++ b/financial/ai-services/src/main/java/oracleai/aiholo/TTSAndAudio2Face.java
@@ -0,0 +1,114 @@
+package oracleai.aiholo;
+
+import com.google.cloud.texttospeech.v1.*;
+import com.google.protobuf.ByteString;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class TTSAndAudio2Face {
+ private static final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ public static void processMetahuman(String fileName, String textToSay, String languageCode, String voiceName) {
+ executor.submit(() -> {
+ try {
+ TTS(fileName, textToSay, languageCode, voiceName);
+ sendToAudio2Face(fileName);
+ } catch (Exception e) {
+ System.out.println("processMetahuman exception during TTS:" + e);
+ //com.google.api.gax.rpc.UnavailableException: io.grpc.StatusRuntimeException:
+ // UNAVAILABLE: Credentials failed to obtain metadata
+ // will occur if token expired
+ //TODO might be funny and helpful to do this, ie have the system gives its status and ask for help ...
+ // sendToAudio2Face("uhoh-lookslikeIneedanewTTStoken.wav");
+ sendToAudio2Face("../audio-aiholo/explainer.wav");
+// sendToAudio2Face("hello-brazil.wav");
+ }
+
+ });
+ }
+
+
+ public static void TTS(String fileName, String text, String languageCode, String voicename) throws Exception {
+ try (TextToSpeechClient textToSpeechClient = TextToSpeechClient.create()) {
+ System.out.println("in TTS languagecode:" + languageCode + " voicename:" + voicename + " text:" + text);
+ SynthesisInput input = SynthesisInput.newBuilder().setText(text).build();
+ // "最受欢迎的游戏是Pods Of Kon。").build();
+ // "最も人気のあるビデオゲームは「Pods Of Kon」です。").build();
+ VoiceSelectionParams voice =
+ VoiceSelectionParams.newBuilder()
+ .setLanguageCode(languageCode) //ja-JP, en-US, ...
+ .setSsmlGender(SsmlVoiceGender.FEMALE) // NEUTRAL, MALE
+ .setName(voicename) // "Kore" pt-BR-Wavenet-D
+ .build();
+ AudioConfig audioConfig =
+ AudioConfig.newBuilder()
+ .setAudioEncoding(AudioEncoding.LINEAR16) // wav AudioEncoding.MP3
+ .build();
+ SynthesizeSpeechResponse response =
+ textToSpeechClient.synthesizeSpeech(input, voice, audioConfig);
+ ByteString audioContents = response.getAudioContent();
+ try (OutputStream out = new FileOutputStream(fileName)) {
+ out.write(audioContents.toByteArray());
+// System.out.println("Audio content written to file:" + fileName);
+ }
+ }
+ }
+
+
+
+
+
+ public static void sendToAudio2Face(String fileName) {
+ System.out.print("sendToAudio2Face for fileName:" + fileName + " ...");
+ RestTemplate restTemplate = new RestTemplate();
+ String baseUrl = "http://localhost:8011/A2F/Player/";
+
+ String setRootPathUrl = baseUrl + "SetRootPath";
+ Map rootPathPayload = new HashMap<>();
+ rootPathPayload.put("a2f_player", "/World/audio2face/Player");
+ rootPathPayload.put("dir_path", AIHoloController.AUDIO_DIR_PATH);
+ sendPostRequest(restTemplate, setRootPathUrl, rootPathPayload);
+
+ String setTrackUrl = baseUrl + "SetTrack";
+ Map trackPayload = new HashMap<>();
+ trackPayload.put("a2f_player", "/World/audio2face/Player");
+ trackPayload.put("file_name", fileName);
+ trackPayload.put("time_range", new int[] { 0, -1 });
+ sendPostRequest(restTemplate, setTrackUrl, trackPayload);
+
+ String playTrackUrl = baseUrl + "Play";
+ Map playPayload = new HashMap<>();
+ playPayload.put("a2f_player", "/World/audio2face/Player");
+ sendPostRequest(restTemplate, playTrackUrl, playPayload);
+ System.out.println(" ...sendToAudio2Face complete");
+ }
+
+ private static void sendPostRequest(RestTemplate restTemplate, String url, Map payload) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity