Skip to content

Commit 8bef931

Browse files
Merge branch 'main' into OperationsAdvisory-reor3
2 parents 4e5e542 + 09bdeff commit 8bef931

31 files changed

Lines changed: 640 additions & 245 deletions

File tree

ai/gen-ai-agents/oci-enterprise-ai-chat/README.md

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# OCI Enterprise AI Agents
1+
# OCI Enterprise AI
22

33
A chat interface and agent hub for [Oracle Cloud Infrastructure Enterprise AI](https://www.oracle.com/artificial-intelligence/enterprise-ai/). Streaming responses, MCP tool integration, OAuth2 SSO, and a full settings workbench to tailor the assistant to each deployment.
44

@@ -21,7 +21,7 @@ Open [http://localhost:3000](http://localhost:3000). On first run, create a `.en
2121
- Node.js 22+ (the Dockerfile uses `node:22-alpine`)
2222
- An OCI tenancy with access to **Generative AI** (Projects, inference, vector & semantic stores) and **Generative AI Agents** (memory, agent flows)
2323
- Locally: `~/.oci/config` with a valid API key
24-
- In a container: **Resource Principal** configured on the Container Instance
24+
- In a Container Instance: **Resource Principal** (or **Instance Principal** for a Compute VM)
2525

2626
---
2727

@@ -39,8 +39,16 @@ OCI_GENAI_PROJECT_ID=ocid1.generativeaiproject.oc1..xxxxx
3939
OCI_CONFIG_FILE=~/.oci/config
4040
OCI_CONFIG_PROFILE=DEFAULT
4141
42-
# Container deployments (Resource Principal instead of config file)
42+
# Container Instance deployments (Resource Principal instead of config file)
4343
USE_RESOURCE_PRINCIPAL=true
44+
45+
# Compute VM deployments (Instance Principal instead of config file)
46+
# USE_INSTANCE_PRINCIPAL=true
47+
48+
# Frontend → backend API base URL. Only needed when the browser cannot reach the
49+
# Next.js API routes via a relative path — e.g. when fronted by an API Gateway
50+
# with a path prefix. Default: same origin + /api.
51+
# NEXT_PUBLIC_GENAI_API_URL=https://your-apigw.example.com/api
4452
```
4553

4654
> **About the Project.** A **Project** is the OCI Generative AI resource that organizes conversations, responses, files, and sandboxes: it's required for any OpenAI-compatible API call. It's also where you configure data retention (max 720h for both responses and conversations), short-term memory compaction, and long-term memory extraction/embedding models. Memory and compaction settings are **set at project creation and cannot be changed later**, so plan ahead. Create one in the OCI Console under *Analytics & AI → Generative AI → Projects*.
@@ -67,7 +75,7 @@ LOG_LEVEL=info
6775

6876
### Models
6977

70-
Models are **discovered dynamically** via `listModels` against the configured compartment. Whatever is enabled in your tenancy shows up in the model picker. No env var needed.
78+
Models are defined as a static list in `src/app/page.js` (`STATIC_MODELS`). To add or remove a model available in your tenancy, edit that array. No env var needed.
7179

7280
---
7381

@@ -85,7 +93,7 @@ The browser hits `middleware.js` first (session guard). If the user has a valid
8593
Real-time SSE streaming, conversation history persisted in OCI Conversation Store and cached locally (IndexedDB), automatic title generation, markdown rendering with code blocks and tables, multi-file attachments.
8694

8795
### Model picker
88-
Switch models per conversation. The list is discovered dynamically from the configured compartment, so anything enabled in your tenancy shows up automatically.
96+
Switch models per conversation. The list is curated in `src/app/page.js` (`STATIC_MODELS`); add the models your tenancy has enabled.
8997

9098
![Model picker](images/06-model-selector.png)
9199

@@ -110,7 +118,7 @@ Toggle per-tool from Settings → Tools → Native:
110118
![Tools settings](images/03-settings-tools.png)
111119

112120
### Custom MCP servers
113-
Add any MCP endpoint (API key, Bearer token, OAuth2 client credentials, or OAuth 2.1). Test the connection, discover tools, and selectively enable individual functions. Tokens persist through a signed-cookie flow, no secrets stored in localStorage.
121+
Add any MCP endpoint (API key, Bearer token, OAuth2 client credentials, or OAuth 2.1). Test the connection, discover tools, and selectively enable individual functions. OAuth 2.1 tokens persist through a signed-cookie flow. API keys and OAuth 2.0 `client_secret` values currently live in `localStorage` — for a production multi-user deployment, consider sourcing them from server-side env vars instead.
114122

115123
![Custom MCP servers](images/03b-settings-tools-custom.png)
116124

@@ -148,7 +156,7 @@ The same chat in dark mode:
148156
### Authentication
149157
- **Oracle IDCS SSO** with OAuth2 Authorization Code (handled by `src/middleware.js` + `src/app/lib/auth.js`)
150158
- **MCP OAuth 2.1** with PKCE for per-tool authorization (`src/app/lib/mcp-oauth.js`)
151-
- Resource Principal auth when running in an OCI Container Instance
159+
- Resource Principal auth when running in an OCI Container Instance, or Instance Principal when running on a Compute VM
152160

153161
### Observability
154162
Langfuse traces every request when `LANGFUSE_*` env vars are set. The Settings → Observability tab provides an in-app trace viewer.
@@ -182,12 +190,15 @@ src/
182190
183191
├── config/
184192
│ ├── app.js # App-wide constants
185-
│ └── darkMode.js # Dark-mode CSS vars + MUI overrides
193+
│ ├── darkMode.js # Dark-mode CSS vars + MUI overrides
194+
│ ├── version.js # APP_VERSION shown in Settings
195+
│ ├── models-internal.js # Internal-only model IDs (empty stub on public branch)
196+
│ └── tools-internal.js # Internal-only marketplace + addons (empty stub on public branch)
186197
187198
├── lib/ # Server-side helpers
188199
│ ├── auth.js # IDCS session + cookie signing
189200
│ ├── mcp-oauth.js # MCP OAuth 2.1 + PKCE
190-
│ ├── oci-auth.js # ConfigFile / ResourcePrincipal
201+
│ ├── oci-auth.js # ConfigFile / Resource Principal / Instance Principal
191202
│ ├── oci-proxy.js # Request signing helpers
192203
│ ├── oci-headers.js # Required OCI headers
193204
│ └── logger.js # Structured logging
@@ -197,8 +208,9 @@ src/
197208
│ ├── conversationStorage.js # IndexedDB cache
198209
│ ├── mcpService.js # MCP client/server mgmt
199210
│ ├── titleService.js # Auto title generation
200-
│ ├── oracleSpeechService.js # Speech integration
201-
│ └── flowService.js # Agent flows
211+
│ ├── oracleSpeechService.js # Oracle speech-to-text
212+
│ ├── speechService.js # Browser speech-to-text fallback
213+
│ └── mockService.js # Mock responses for offline demos
202214
203215
├── hooks/
204216
│ └── useChat.js # Streaming chunk processing + state
@@ -232,7 +244,7 @@ src/
232244
![SSO sequence](images/sso-flow.png)
233245

234246
### MCP tool invocation
235-
1. OCI executes MCP tools natively via the Responses API. the app **does not run tools itself**
247+
1. OCI executes MCP tools natively via the Responses API. The app **does not run tools itself** during chat (only for Test Connection / Refresh in Settings).
236248
2. For OAuth 2.1 MCP servers (e.g. SDD Generator), the client obtains an access token via `/api/mcp/oauth/*` and passes it to OCI in the tool's `authorization` field
237249
3. Custom servers use the more generic `/api/mcp` JSON-RPC proxy for discovery and direct invocation (outside OCI)
238250

@@ -288,11 +300,10 @@ The app runs as a **standalone Next.js build** inside an [OCI Container Instance
288300
```
289301
ALL {resource.type='computecontainerinstance', resource.compartment.id='<compartment-ocid>'}
290302
```
291-
4. **IAM policies**: `Identity → Policies → Create`. Grant the dynamic group access to GenAI, Conversation Store, and any other services you use:
303+
4. **IAM policies**: `Identity → Policies → Create`. Grant the dynamic group access to GenAI:
292304
```
293305
allow dynamic-group <dg-name> to use generative-ai-family in compartment <compartment-name>
294306
allow dynamic-group <dg-name> to manage genai-agent-family in compartment <compartment-name>
295-
allow dynamic-group <dg-name> to manage objects in compartment <compartment-name>
296307
```
297308
5. **(SSO only)** Register the app in **IDCS** as a Confidential Application with redirect URI `https://<your-domain>/api/auth/callback/oci`. Save the Client ID + Secret for the env vars.
298309

@@ -348,6 +359,29 @@ oci container-instances container-instance restart \
348359
- When the LB-to-backend connection is HTTP (not HTTPS end-to-end), cookies must **not** carry the `Secure` flag. `mcp-oauth.js` and the IDCS auth code already handle this automatically when running over HTTP.
349360
- All secrets (Client Secret, Session Secret, Langfuse keys) should be set as **environment variables on the Container Instance**, never baked into the image.
350361

362+
### Alternative: deploy on a Compute VM
363+
364+
The app also runs on a regular OCI Compute instance using **Instance Principal** auth. Same flow as above with three differences:
365+
366+
1. **Dynamic Group**: match the Compute instance instead of Container Instances, e.g.
367+
```
368+
ALL {instance.compartment.id = '<compartment-ocid>'}
369+
```
370+
or pin to a specific instance: `ALL {instance.id = 'ocid1.instance.oc1...'}`
371+
2. **No OCIR / image step**: clone the repo on the VM and run `npm install && npm run build && npm start` (or use a systemd unit / pm2). The Dockerfile + image push are not needed.
372+
3. **Env var**: set `USE_INSTANCE_PRINCIPAL=true` instead of `USE_RESOURCE_PRINCIPAL=true`.
373+
374+
The IAM policies and the Load Balancer setup are identical.
375+
376+
### Fronting the app with an API Gateway
377+
378+
If you put an **API Gateway** in front of the app instead of (or in addition to) a Load Balancer, configure the deployment to forward two headers to the backend:
379+
380+
- `Host` — the original public hostname the client used
381+
- `X-Forwarded-Proto``https`
382+
383+
Without these, `getBaseUrl()` builds the wrong `redirect_uri` (using the gateway's internal hostname) and IDCS rejects the SSO callback with `redirect_uri_mismatch`. The same applies to the Load Balancer setup — both should pass `Host` and `X-Forwarded-Proto` through.
384+
351385
---
352386

353387
## Commands
@@ -365,7 +399,7 @@ npm run test # Playwright tests
365399
## Troubleshooting
366400

367401
**`Invalid value for required field 'model'`**
368-
The model you selected is not available on the OpenAI-compatible endpoint. Pick a different model in Settings → AI, or extend the app to hit the native endpoint.
402+
The model you selected is not available on the OpenAI-compatible endpoint. Pick a different model from the model selector in the chat header, or extend the app to hit the native endpoint.
369403

370404
**`OCI_COMPARTMENT_ID is required`**
371405
Missing env var. Set it in `.env.local` for local dev or in the Container Instance env for deployment.
@@ -391,7 +425,7 @@ Harmless. Emitted during SSG when charts render with zero-size containers; they
391425
- **Framework** — Next.js 16 (App Router, standalone output, Turbopack)
392426
- **UI** — React 19, MUI v7, Framer Motion, Lucide icons
393427
- **Charts** — Recharts
394-
- **OCI**`oci-sdk` (ConfigFile / Resource Principal auth)
428+
- **OCI**`oci-sdk` (ConfigFile / Resource Principal / Instance Principal auth)
395429
- **Protocols** — Server-Sent Events, JSON-RPC 2.0 (MCP), OAuth 2.1 + PKCE
396430
- **Observability** — Langfuse (optional)
397431
- **Testing** — Playwright
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
.next/cache
3+
.git
4+
docs
5+
*.md
6+
.env*

ai/gen-ai-agents/oci-enterprise-ai-chat/files/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "oci-agent-light-demo-creator",
3-
"version": "0.3.2",
3+
"version": "0.4.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",

ai/gen-ai-agents/oci-enterprise-ai-chat/files/src/app/api/responses/route.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ export async function POST(request) {
6565
}
6666
}
6767

68-
// Chain to a previous response (for function calling round-trips)
68+
// Chain to a previous response (for function calling round-trips or
69+
// mcp_approval_response continuations). OCI rejects `previous_response_id`
70+
// and `conversation` together (400 mutually_exclusive_parameters), so the
71+
// previous-response id wins when both are present.
6972
if (previousResponseId) {
7073
requestBody.previous_response_id = previousResponseId;
74+
if (requestBody.conversation) delete requestBody.conversation;
7175
}
7276

7377
log.info('OCI request payload', {
@@ -356,6 +360,12 @@ export async function POST(request) {
356360
// Lifecycle events (created, in_progress) — debug level
357361
if (eventType === 'response.created' || eventType === 'response.in_progress') {
358362
log.debug('Response lifecycle', { event: eventType, elapsed, responseId: data.response?.id });
363+
// Expose response.id to the client as soon as we know it so a UI that needs
364+
// to chain (e.g. mcp_approval_response) has the previous_response_id ready
365+
// before the stream completes.
366+
if (eventType === 'response.created' && data.response?.id) {
367+
controller.enqueue(encoder.encode(JSON.stringify({ response_id: data.response.id }) + '\n'));
368+
}
359369
}
360370

361371
// ═══ TEXT STREAMING ═══
@@ -376,6 +386,21 @@ export async function POST(request) {
376386
mcp: { type: 'connecting', server: data.item.server_label }
377387
}) + '\n'));
378388
}
389+
else if (itemType === 'mcp_approval_request') {
390+
// Human-in-the-loop: tool call awaiting user approval.
391+
// The frontend renders a card with Approve/Reject buttons; on click
392+
// it chains a new request with previous_response_id and an input of
393+
// [{type:'mcp_approval_response', approval_request_id, approve}].
394+
log.info('MCP approval request', { tool: data.item.name, server: data.item.server_label, id: itemId });
395+
controller.enqueue(encoder.encode(JSON.stringify({
396+
mcp_approval_request: {
397+
request_id: itemId,
398+
server_label: data.item.server_label,
399+
tool_name: data.item.name,
400+
arguments: data.item.arguments,
401+
}
402+
}) + '\n'));
403+
}
379404
else if (itemType === 'mcp_call') {
380405
toolCalls.push({ id: itemId, tool: data.item.name, server: data.item.server_label });
381406
trace.tools[itemId] = { tool: data.item.name, server: data.item.server_label, status: 'calling', startMs: elapsed };

0 commit comments

Comments
 (0)