Everything you need to know to build and run the Interactive AI Holograms exhibit...
See blog for details here
Try these to trigger specific agents (via the web UI text box, or by holding the Z key and speaking):
| Agent | Example question |
|---|---|
| LLM only (the default) | "What is a vector search?" |
| Spring AI Vector RAG | "Search documents about Oracle database features" |
| In-DB ONNX Vector RAG | "Search docs for Oracle database security best practices" |
| Mirror Me | "Mirror me" |
| Financial | "Describe my stock portfolio, use financial agent" |
| Clear History | "Clear history" |
- Database, Speech AI, and LLM Setup as described in the next section below
- Java 21 or newer
- The aiholo.jar file downloaded from here
- An
.envfile or exported environment variables for your deployment (see configuration section below and .env_example)
Run Oracle 26ai Free with native VECTOR support using Docker:
docker run -d \
--name oracle-free \
--restart unless-stopped \
-p 1521:1521 \
-e ORACLE_PWD=<your-password> \
-v /path/to/data/oracle:/opt/oracle/oradata \
--cpus="8.0" \
--memory="64g" \
container-registry.oracle.com/database/free:latestConnection details:
- Host:
localhost(or your server IP for remote access) - Port:
1521 - Service:
FREEPDB1 - User:
SYSTEMorPDBADMIN - Password:
<your-password>
Note: For a complete development stack with Python FastAPI + Spring Boot + Oracle Vector Search, see the oracleaidatabase-devenv-util folder.
Install Ollama and pull recommended models:
# Install Ollama
curl -fsSL https://ollama.com/install.sh | sh
# Pull recommended models
ollama pull qwen2.5:7b # Best overall
ollama pull llama3.2 # Fast general-purpose model (3B params)
ollama pull mistral # Strong reasoning model (7B params)
ollama pull nomic-embed-text # Text embedding model for RAG
# Verify installation
ollama list
# test model with ollama or via API...
ollama run llama3.2
curl http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Say hello in one sentence."
}'API endpoint: http://localhost:11434
To accept requests from other machines (required for database integration), set the host to listen on all interfaces:
# Set environment variable
export OLLAMA_HOST=0.0.0.0:11434
# Restart Ollama service
systemctl restart ollama # On Linux with systemd
# or
pkill ollama && ollama serve # Manual restart
# Verify it's listening on all interfaces
netstat -tlnp | grep 11434For in-database Ollama integration with Oracle, ensure Ollama is accessible from the database server and configure using DBMS_CLOUD_AI or DBMS_VECTOR_CHAIN.
Install and configure Google Cloud SDK for speech-to-text and text-to-speech:
# Install gcloud CLI
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
# Initialize and authenticate
gcloud init
gcloud auth application-default login
# Enable required APIs
gcloud services enable speech.googleapis.com
gcloud services enable texttospeech.googleapis.com
# Set project (replace with your project ID)
gcloud config set project YOUR_PROJECT_IDRequired environment variables:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-key.json
export GOOGLE_CLOUD_PROJECT=your-project-idFor fully offline STT and TTS, use Whisper and Piper instead of Google Cloud.
Whisper STT — local speech-to-text via an OpenAI-compatible API server:
# Install faster-whisper-server (Python)
pip install faster-whisper-server
# Start the server
faster-whisper-server --host 0.0.0.0 --port 8000STT_ENGINE=WHISPER
WHISPER_URL=http://localhost:8000
WHISPER_MODEL=baseModels are downloaded automatically on first use. Larger models (small, medium, large-v3) are more accurate but slower.
Piper TTS — fast local text-to-speech using ONNX models:
- Download Piper from GitHub releases
- Download a voice model from HuggingFace (
.onnx+.onnx.jsonfiles)
TTS_ENGINE=PIPER
PIPER_EXE_PATH=/path/to/piper
PIPER_MODEL_PATH=/path/to/en_US-kathleen-low.onnxFully offline stack — complete .env snippet for zero-internet operation:
# LLM — local Ollama
DEFAULT_LLM_PROVIDER=ollama
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2:3b
# STT — local Whisper
STT_ENGINE=WHISPER
WHISPER_URL=http://localhost:8000
WHISPER_MODEL=base
# TTS — local Piper
TTS_ENGINE=PIPER
PIPER_EXE_PATH=/path/to/piper
PIPER_MODEL_PATH=/path/to/en_US-kathleen-low.onnxNo API keys, no cloud accounts, no internet required. Combine with Oracle Database for in-DB vector RAG to complete the stack.
Note: Piper is recommended over Coqui for local TTS. Coqui TTS (the company) shut down in 2023 and the open-source project is no longer maintained. Piper is actively developed, faster on CPU, has more voice models, and requires no Python runtime.
java -jar aiholo.jarIf your environment is set correctly, the app will start and serve the UI on:
http://localhost:8082/aiholo
There are two ways to send questions to the application. Both go through the same AgentService.processQuestion() pipeline, so ENABLED_AGENTS, AGENT_ROUTING_MODE, and all other agent settings apply equally to both.
| Input method | How it works | Config |
|---|---|---|
| Web UI | Browser-based interface at /aiholo — type a question or click the microphone to record audio |
Always available |
| Hotkey / Voice Assistant | System-wide hotkeys (Z = speak, X = speak + webcam, A hold = stop audio) and optional wake-word detection (Porcupine or OpenWakeWord) | ENABLE_GLOBAL_HOTKEY, ENABLE_VOICE_ASSISTANT |
The JAR reads configuration from environment variables.
Minimum commonly needed settings:
SERVER_PORT=8082
AIHOLO_HOST_URL=http://localhost:8082
# LLM provider: openai, claude, ollama, gemini
DEFAULT_LLM_PROVIDER=openai
OPENAI_API_KEY=your-openai-api-key
# For local/offline LLM instead:
# DEFAULT_LLM_PROVIDER=ollama
# OLLAMA_URL=http://localhost:11434
# OLLAMA_MODEL=mistral:latest
DB_USER=admin
DB_PASSWORD=your-database-password
DB_URL=jdbc:oracle:thin:@yourdb_high?TNS_ADMIN=/path/to/Wallet_yourdb
OUTPUT_FILE_PATH=/path/to/aiholo_output.txtIf you are using voice assistant features, also set:
ENABLE_VOICE_ASSISTANT=true
VOICE_ASSISTANT_ENGINE=porcupine
PORCUPINE_ACCESS_KEY=your-porcupine-key
KEYWORD_PATH=/path/to/Hey-computer.ppnOr with OpenWakeWord:
ENABLE_VOICE_ASSISTANT=true
VOICE_ASSISTANT_ENGINE=openwakeword
OPENWAKEWORD_SCRIPT_PATH=wakeupwords/openwakeword_bridge.py
OPENWAKEWORD_MODEL=hey_jarvis- The packaged artifact name is
aiholo.jar - Run from the repo root, or adjust the path to the JAR accordingly
- Keep
SERVER_PORTandAIHOLO_HOST_URLaligned - Default local credentials are
oracleai / oracleai
PowerShell:
$env:OPENAI_API_KEY="your-openai-api-key"
$env:DB_USER="admin"
$env:DB_PASSWORD="your-database-password"
$env:DB_URL="jdbc:oracle:thin:@yourdb_high?TNS_ADMIN=C:\path\to\Wallet_yourdb"
$env:SERVER_PORT="8082"
$env:AIHOLO_HOST_URL="http://localhost:8082"
java -jar target/aiholo.jarBash:
export OPENAI_API_KEY="your-openai-api-key"
export DB_USER="admin"
export DB_PASSWORD="your-database-password"
export DB_URL="jdbc:oracle:thin:@yourdb_high?TNS_ADMIN=/path/to/Wallet_yourdb"
export SERVER_PORT="8082"
export AIHOLO_HOST_URL="http://localhost:8082"
java -jar target/aiholo.jarENABLED_AGENTS is a comma-separated list of agent valueNames.
Examples:
ENABLED_AGENTS=visionagent,shipsagentENABLED_AGENTS=visionagent,shipsagent,equipmentagentBehavior:
ENABLED_AGENTSshould be set in your.envfile — it controls which agents are active- Only agents whose
valueNameappears in the list will load - The fallback path (
DirectLLMAgent/DefaultFallbackAgent) is always registered regardless - Custom
@Componentagents are also discovered and filtered by theirvalueName - This filter applies to all input methods — both the web UI and the hotkey/voice assistant go through
AgentService.processQuestion(), so the same set of agents is available everywhere
The sample env documents these values:
ENABLED_AGENTS value |
Purpose |
|---|---|
clearhistory |
Clears conversation history |
mirrormeagent |
Mirror/digital double behavior |
shipsagent |
Navy ship lookup |
equipmentagent |
Navy equipment lookup |
digitaltwinagent |
Digital twin actions |
signagent |
Sign/display output actions |
visionagent |
Vision/image analysis |
aitoolkitagent |
Sandbox/toolkit integration |
financialagent |
Financial flow integration |
gameragent |
Game-oriented routing |
indbonnxvectorrag |
In-DB ONNX vector RAG via Oracle SQL function |
image |
Generate images from text via OpenAI DALL-E |
editimage |
Capture webcam photo, modify it per user instruction via GPT-4o + DALL-E |
imageneditimage |
Capture webcam photo, edit the actual image via Google Vertex AI Imagen |
springaivectorrag |
Spring AI VectorStore RAG with OpenAI embeddings |
dbsqlagent |
Natural language to SQL via DBMS_CLOUD_AI |
dbsummarizationagent |
In-database summarization via DBMS_VECTOR_CHAIN |
dbpropertygraphagent |
Property graph queries via SQL/PGQ |
springaichatagent |
Spring AI ChatClient with Oracle DB grounding |
langchain4joraclerag |
Langchain4j OracleEmbeddingStore RAG |
langchain4jtoolagent |
Langchain4j tool/function-calling with Oracle DB |
generalagent |
Fallback routing value used by the built-in fallback agents |
Notes:
- Both
DirectLLMAgentandDefaultFallbackAgentcurrently usegeneralagentas theirvalueName - Built-in agents register before auto-discovered custom agents, so built-ins win when trigger keywords overlap
By default, questions are routed to agents via keyword matching (getKeywords()). You can optionally let the configured LLM pick the best agent instead:
# keyword = fast, deterministic keyword matching (default)
# llm = LLM reads each agent's description and picks the best match
AGENT_ROUTING_MODE=keywordWhen AGENT_ROUTING_MODE=llm, the system builds a prompt listing all registered agents with their getAgentDescription() text and asks the LLM to return the index of the best match. If the LLM returns "none" or fails, it falls back to keyword matching automatically.
Trade-offs:
keyword— instant, no extra LLM call, deterministic, requires users to use trigger phrasesllm— handles natural phrasing ("can you look at that ISO doc?" routes to the RAG agent), but adds one LLM round-trip per question
The fallback/default agent supports multiple LLM providers, controlled by a single env var:
| Provider | DEFAULT_LLM_PROVIDER |
API Key | Model env var | Default model |
|---|---|---|---|---|
| OpenAI | openai (default) |
OPENAI_API_KEY |
OPENAI_MODEL |
gpt-4 |
| Ollama (local) | ollama |
None needed | OLLAMA_MODEL |
mistral:latest |
| Claude | claude |
CLAUDE_API_KEY |
CLAUDE_MODEL |
claude-sonnet-4-20250514 |
| Gemini | gemini |
GEMINI_API_KEY |
GEMINI_MODEL |
gemini-2.0-flash |
For fully offline operation, use Ollama:
DEFAULT_LLM_PROVIDER=ollama
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2:3bThe provider is set at startup. The LLMService handles all REST API differences internally — agents just call llmService.query(prompt).
The following agents use Oracle Database 23ai. The key axis is where inference runs (in-DB vs external) and which framework manages the interaction (raw JDBC, Spring AI, Langchain4j).
* SpringAIVectorRAGAgent is the most straightforward starting point — it uses standard Spring AI APIs with OpenAI embeddings and requires minimal database-side setup. The examples below are included in the main library/distribution and are meant to be customized for your use case.
| Agent Name | Description | Framework | LLM Location | DB Role | DB Prep Required | valueName |
|---|---|---|---|---|---|---|
| InDBOnnxVectorRAGAgent | Full in-database RAG: ONNX embeddings + Ollama or other LLM, all via a single SQL function call | JdbcTemplate | In-database (Ollama) | RAG + inference | Run setup_onnx_vector_rag.sql (creates ONNX model, vector table, and SQL function) |
indbonnxvectorrag |
| SpringAIVectorRAGAgent * | Spring AI vector similarity search with OpenAI embeddings stored in Oracle | Spring AI | External (OpenAI) | Vector store only | Table auto-created by Spring AI on startup | springaivectorrag |
| DBSQLAgent | Natural language to SQL — asks questions in English, gets answers from relational data | JdbcTemplate | In-database (DBMS_CLOUD_AI) | NL-to-SQL | Run setup_dbms_cloud_ai.sql (configures AI profile and credentials) |
dbsqlagent |
| DBSummarizationAgent | In-database document summarization without leaving Oracle | JdbcTemplate | In-database (DBMS_VECTOR_CHAIN) | Summarization | Run setup_vector_chain.sql (configures LLM credential for DBMS_VECTOR_CHAIN) |
dbsummarizationagent |
| DBPropertyGraphAgent | Relationship-based queries using SQL/PGQ graph pattern matching | JdbcTemplate | N/A (SQL/PGQ) | Graph queries | Run setup_property_graph.sql (CREATE PROPERTY GRAPH over your tables) |
dbpropertygraphagent |
| SpringAIChatAgent | Spring AI ChatClient with optional Oracle DB context for grounded responses | Spring AI | External (OpenAI) | Tool/grounding backend | None (uses existing tables for optional context) | springaichatagent |
| Langchain4jOracleRAGAgent | Langchain4j vector search using OracleEmbeddingStore | Langchain4j | External (any) | Vector store (OracleEmbeddingStore) | Table auto-created by Langchain4j OracleEmbeddingStore.builder() |
langchain4joraclerag |
| Langchain4jToolAgent | Langchain4j tool/function-calling pattern backed by Oracle DB queries | Langchain4j | N/A | Tool/function backend | None (queries existing schema metadata) | langchain4jtoolagent |
All database-backed agents share a single DataSource configured via DataSourceConfiguration (using DB_USER, DB_PASSWORD, DB_URL environment variables). Currently only one shared database can be specified; however, agents can create additional DataSource instances programmatically if they need to connect to a different database.
The SpringAIVectorRAGAgent uses OracleDBVectorStore, which manages the vector store table in Oracle Database. The table is auto-created at startup if it does not already exist. The schema is:
| Column | Type | Description |
|---|---|---|
id |
NUMBER GENERATED AS IDENTITY (PK) |
Auto-generated row ID |
text |
CLOB |
The document text chunk |
embeddings |
VECTOR |
OpenAI embedding vector (stored using Oracle 23ai native VECTOR type) |
metadata |
JSON |
Document metadata (source file, page number, etc.) |
PDFs are uploaded via the /vectorrag UI, split into chunks by TokenTextSplitter, embedded using OpenAI's text-embedding-ada-002, and inserted into the table. Similarity search uses Oracle's built-in vector distance functions (COSINE_DISTANCE, L2_DISTANCE, etc.) with no external vector database required.
The table name, distance metric, and startup behavior are configured in application.yaml:
vectorrag:
table-name: ${VECTORRAG_TABLE_NAME:vector_store}
drop-at-startup: ${VECTORRAG_DROP_AT_STARTUP:false}
distance-metric: ${VECTORRAG_DISTANCE_METRIC:COSINE}
temp-dir: ${VECTORRAG_TEMP_DIR:tempDir}These can also be overridden via environment variables (VECTORRAG_TABLE_NAME, VECTORRAG_DISTANCE_METRIC, etc.). The default table name is vector_store and the default distance metric is COSINE.
Custom agents are auto-discovered if they:
- implement
Agent - are on the application classpath (place the
.javafile undersrc/main/java/oracleai/aiholo/agents/in the source tree, or add the compiled.class/.jarto the classpath via-cpor by dropping it into the Spring Boot loader'sBOOT-INF/classes/directory) - are annotated with
@Component
Minimal example:
package oracleai.aiholo.agents;
import org.springframework.stereotype.Component;
@Component
public class WeatherAgent implements Agent {
@Override
public String getName() {
return "Weather Agent";
}
@Override
public String getValueName() {
return "weatheragent";
}
@Override
public String getAgentDescription() {
return "Answers questions about current weather, forecasts, and temperature.";
}
@Override
public String[][] getKeywords() {
return new String[][] {
{"weather"},
{"forecast"},
{"temperature"}
};
}
@Override
public String processQuestion(String question) {
return "Weather agent received: " + question;
}
@Override
public boolean isConfigured() {
return true;
}
}At startup, AgentService:
- registers built-in agents
- registers fallback agents
- scans the Spring application context for additional
Agentbeans - skips any duplicate
valueName
If you want your custom agent to participate in ENABLED_AGENTS, set a stable valueName and use that exact value in .env:
ENABLED_AGENTS=weatheragentgetValueName()should be lowercase and stablegetKeywords()is simple keyword-set matching, not semantic routing- Empty
getKeywords()makes an agent behave like a fallback - Use unique keywords if you do not want a built-in agent to match first
- Return
falsefromisConfigured()when required credentials or dependencies are missing
Contact Paul Parkinson with any questions or recommendations.
