diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c7e180..839dd2ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,12 +75,12 @@ held the same primary keys. Updated 7 files to match their current notebook number: -- `notebook_16_agent_memory.py` (was `locus_notebook_10_*`) -- `notebook_35_deepagent.py` (was `locus_notebook_30_medical`) -- `notebook_44_rag_basics.py` (was `locus_notebook_39_*`) -- `notebook_45_rag_providers.py` (was `locus_notebook_40_*`) -- `notebook_46_rag_agents.py` (was `locus_notebook_41_*`) -- `notebook_54_checkpoint_backends.py` (was `locus_notebook_48`) +- `notebook_08_agent_memory.py` (was `locus_notebook_10_*`) +- `notebook_29_deepagent.py` (was `locus_notebook_30_medical`) +- `notebook_38_rag_basics.py` (was `locus_notebook_39_*`) +- `notebook_39_rag_providers.py` (was `locus_notebook_40_*`) +- `notebook_40_rag_agents.py` (was `locus_notebook_41_*`) +- `notebook_52_checkpoint_backends.py` (was `locus_notebook_48`) - `notebook_68_agent_server.py` (was `locus_notebook_62`) A pre-commit / lint check to prevent future drift is tracked in #241. @@ -125,8 +125,8 @@ reasoning. Cost: Oracle-only, alpha upstream (`oracleagentmemory` - New optional dep `[agentmemory]` in `pyproject.toml` (`oracleagentmemory>=26.4.0,<27`). Also rolled into `[checkpoints]` so `pip install locus-sdk[checkpoints]` covers everything Oracle. -- New live notebook `examples/notebook_13_oracle_agent_memory.py` + - matching docs page `docs/notebooks/notebook_13_oracle_agent_memory.md`, +- New live notebook `examples/notebook_09_oracle_agent_memory.py` + + matching docs page `docs/notebooks/notebook_09_oracle_agent_memory.md`, filed in sequential order in the Oracle 26ai notebook block (slot 13). - New workbench pattern `oracle_agent_memory` (PATTERNS + PATTERN_RUNNERS in `workbench/backend/runner.py`), filed in sequential order after @@ -191,26 +191,26 @@ wrapping someone else's bindings rather than Locus driving them. Every one of those seven notebooks now puts a Locus `Agent` in the driver's seat with Oracle 26ai as the durable substrate: -- **06 — `notebook_06_oracle_26ai_rag.py`**: `create_rag_tool` over +- **06 — `notebook_41_oracle_26ai_rag.py`**: `create_rag_tool` over `OracleVectorStore`; the agent decides when to call `VECTOR_DISTANCE` and grounds its answer in the returned passages. -- **07 — `notebook_07_oracle_26ai_checkpointer.py`**: `Agent(checkpointer= +- **07 — `notebook_53_oracle_26ai_checkpointer.py`**: `Agent(checkpointer= oracle_checkpointer(...))`; a brand-new Agent instance resumes the same `thread_id` and recalls turn 1 — no manual save/load. -- **08 — `notebook_08_oracle_adb_loader.py`**: `@tool fetch_articles_by_topic` +- **08 — `notebook_42_oracle_adb_loader.py`**: `@tool fetch_articles_by_topic` wrapping `OracleADBLoader` with `bind_params`; the agent picks the topic and runs the parameter-bound SELECT. -- **09 — `notebook_09_oracle_indb_chunker.py`**: agent with +- **09 — `notebook_43_oracle_indb_chunker.py`**: agent with `chunk_paragraph` + `chunk_table_rows` tools over `OracleInDBChunker`; picks the right primitive per prompt. -- **10 — `notebook_10_oracle_indb_embeddings.py`**: data-residency RAG +- **10 — `notebook_44_oracle_indb_embeddings.py`**: data-residency RAG agent on `OracleInDBEmbeddings` + `OracleVectorStore` — embedding + similarity search both stay inside 26ai. -- **11 — `notebook_11_oracle_store.py`**: `Agent(memory_manager= +- **11 — `notebook_10_oracle_store.py`**: `Agent(memory_manager= LLMMemoryManager(store=OracleStore))`; thread A teaches a fact, brand-new Agent in thread B recalls it without seeing thread A's messages. -- **12 — `notebook_12_oracle_versioned_saver.py`**: three-turn agent +- **12 — `notebook_54_oracle_versioned_saver.py`**: three-turn agent with both `oracle_checkpointer` (live thread) and `OracleCheckpointSaver` (versioned lineage); shows `list_checkpoints`, `get(checkpoint_id=...)`, and `put_writes`. diff --git a/README.md b/README.md index 7fb56148..211344d0 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ The seven primitives: The contracts live in locus, the SQL is generated locally, and the only runtime requirement is `python-oracledb` thin mode. -→ [Notebook 06 — Oracle 26ai RAG](https://locusagents.oracle.com/notebooks/notebook_06_oracle_26ai_rag/) · [Notebook 07 — Oracle 26ai checkpointer](https://locusagents.oracle.com/notebooks/notebook_07_oracle_26ai_checkpointer/) · [Notebooks 08-12 — loader, chunker, embeddings, store, versioned saver](https://locusagents.oracle.com/notebooks/) +→ [Notebook 06 — Oracle 26ai RAG](https://locusagents.oracle.com/notebooks/notebook_41_oracle_26ai_rag/) · [Notebook 07 — Oracle 26ai checkpointer](https://locusagents.oracle.com/notebooks/notebook_53_oracle_26ai_checkpointer/) · [Notebooks 08-12 — loader, chunker, embeddings, store, versioned saver](https://locusagents.oracle.com/notebooks/) --- @@ -335,10 +335,10 @@ git clone https://github.com/oracle-samples/locus.git cd locus && pip install -e . python examples/notebook_01_oci_transports.py # start here — three OCI transports -python examples/notebook_06_oracle_26ai_rag.py # native VECTOR RAG on Oracle 26ai -python examples/notebook_07_oracle_26ai_checkpointer.py # durable agent threads in ADB -python examples/notebook_14_basic_agent.py # your first agent -python examples/notebook_35_deepagent.py # deep-research factory +python examples/notebook_41_oracle_26ai_rag.py # native VECTOR RAG on Oracle 26ai +python examples/notebook_53_oracle_26ai_checkpointer.py # durable agent threads in ADB +python examples/notebook_06_basic_agent.py # your first agent +python examples/notebook_29_deepagent.py # deep-research factory python examples/notebook_69_research_workflow.py # full research pipeline ``` diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 9a3cfc9c..96faa8e1 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -9,7 +9,7 @@ Everything `locus` ships, what it does, and where to find it. - **Idempotent tools** — `@tool(idempotent=True)` dedupes on `(name, args)` inside the loop. No double-charge, double-book, double-page. - **Reasoning loop nodes** — Reflexion, Grounding, Causal as first-class Think → Execute → **Reflect** → Think nodes, not bolted-on libraries. - - **GSAR** — typed-grounding layer from [arXiv:2604.23366](https://arxiv.org/abs/2604.23366) with four-way claim partition + tiered replanning. + - **GSAR** — typed-grounding layer from [Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366) with four-way claim partition + tiered replanning. - **Termination algebra** — `MaxIterations(10) | TextMention("DONE") & ConfidenceMet(0.9)` is real Python (`__or__` / `__and__` operator overloads). - **Six multi-agent shapes plus A2A** — Composition, Orchestrator, Swarm, Handoff, StateGraph, Functional + A2A for cross-process meshes. - **Cognitive router (PRISM)** — NL → typed `GoalFrame` → typed `ProtocolRegistry` → `PolicyGate` → compiled orchestration. LLM fills a schema; 8 built-in protocols; zero topology hand-writing. @@ -26,7 +26,7 @@ Everything `locus` ships, what it does, and where to find it. | **Reflexion** | Self-evaluation node in the ReAct cycle; rewrites the next turn when the last one was wrong | `Agent(reflexion=True)` · [Reasoning](concepts/reasoning.md) | | **Grounding** | LLM-as-judge claim verification against tool results; below-threshold triggers replanning | `Agent(grounding=True)` · [Reasoning](concepts/reasoning.md) | | **Causal chains** | Cause-effect graph builder with cycle/contradiction detection | `locus.reasoning.causal.CausalChain` · [Reasoning](concepts/reasoning.md) | -| **GSAR** | Typed-grounding safety layer (arXiv:2604.23366) — four-way claim partition + tiered replanning | `Agent(gsar=GSARConfig(...))` · [GSAR](concepts/gsar.md) | +| **GSAR** | Typed-grounding safety layer ([Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366)) — four-way claim partition + tiered replanning | `Agent(gsar=GSARConfig(...))` · [GSAR](concepts/gsar.md) | | **Cancel** | Thread-safe abort during a run; emits `TerminateEvent` with reason | `agent.cancel()` · [Agent loop](concepts/agent-loop.md) | | **Interrupts (HITL)** | Pause via `InterruptEvent`; resume with `agent.resume(...)` | `locus.core.interrupt` · [Interrupts](concepts/interrupts.md) | | **Structured output** | Pass `output_schema=` (Pydantic), final answer is parsed into a typed instance | `locus.agent.config`, `locus.core.structured` · [Structured output](concepts/structured-output.md) | @@ -90,6 +90,7 @@ Everything `locus` ships, what it does, and where to find it. | **CircuitBreaker executor** | Auto-disable a tool after N failures | `locus.tools.executor` · [Executors](concepts/executors.md) | | Result-store offload | Move large tool results to object storage; agent sees a pointer | `locus.tools.result_storage` | | Path / URL safety | Validate filesystem and network access from tool args | `locus.tools.path_safety`, `locus.tools.url_safety` · [Safety](concepts/safety.md) | +| **`use_oci` + `describe_oci`** | Open-spec built-ins: drive the whole OCI control plane (~190 services) from one agent — discover with `describe_oci`, execute with `use_oci`; read-only by default | `locus.tools.use_oci`, `locus.tools.describe_oci` · [Tools](concepts/tools.md#open-spec-built-ins-for-the-oracle-estate) | | **MCP — client + server** | Talk to / be talked to by Anthropic-spec MCP servers | `locus.integrations.fastmcp` · [MCP](concepts/mcp.md) | ## Memory — checkpointer backends diff --git a/docs/capabilities.md b/docs/capabilities.md index e26cb5ae..2126b89e 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -27,9 +27,9 @@ Everything `locus` ships, what it does, and where to find it. - **Reasoning loop nodes** — Reflexion, Grounding, Causal as first-class Think → Execute → **Reflect** → Think nodes, not bolted-on libraries. - **GSAR** — typed-grounding safety layer from - [arXiv:2604.23366](https://arxiv.org/abs/2604.23366): four-way claim - partition (grounded / ungrounded / contradicted / complementary) + tiered - replanning decisions. + [Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366): + four-way claim partition (grounded / ungrounded / contradicted / + complementary) + tiered replanning decisions. - **Termination algebra** — `MaxIterations(10) | TextMention("DONE") & ConfidenceMet(0.9)` is real Python (`__or__` / `__and__` overloads). Greppable, unit-testable, serialisable. - **Idempotent tools** — `@tool(idempotent=True)` dedupes on `(name, args)` inside the Execute node. No double-charge, double-book, double-page — even on model retry or checkpoint resume. - **OCI, OpenAI, and Anthropic-compatible providers** — OCI Generative AI @@ -48,7 +48,7 @@ Everything `locus` ships, what it does, and where to find it. | **Reflexion** | Self-evaluation node in the ReAct cycle; rewrites the next turn when the last one was wrong | `Agent(reflexion=True)` · [Reasoning](concepts/reasoning.md) | | **Grounding** | LLM-as-judge claim verification against tool results; below-threshold triggers replanning | `Agent(grounding=True)` · [Reasoning](concepts/reasoning.md) | | **Causal chains** | Cause-effect graph builder with cycle/contradiction detection | `locus.reasoning.causal.CausalChain` · [Reasoning](concepts/reasoning.md) | -| **GSAR** | Typed-grounding safety layer (arXiv:2604.23366) — four-way claim partition + tiered replanning | `Agent(gsar=GSARConfig(...))` · [GSAR](concepts/gsar.md) | +| **GSAR** | Typed-grounding safety layer ([Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366)) — four-way claim partition + tiered replanning | `Agent(gsar=GSARConfig(...))` · [GSAR](concepts/gsar.md) | | **Cancel** | Thread-safe abort during a run; emits `TerminateEvent` with reason | `agent.cancel()` · [Agent loop](concepts/agent-loop.md) | | **Interrupts (HITL)** | Pause via `InterruptEvent`; resume with `agent.resume(...)` | `locus.core.interrupt` · [Interrupts](concepts/interrupts.md) | | **Structured output** | Pass `output_schema=` (Pydantic), final answer is parsed into a typed instance | `locus.agent.config`, `locus.core.structured` · [Structured output](concepts/structured-output.md) | diff --git a/docs/concepts/checkpointers.md b/docs/concepts/checkpointers.md index 42bbe05e..e79f0f94 100644 --- a/docs/concepts/checkpointers.md +++ b/docs/concepts/checkpointers.md @@ -183,7 +183,7 @@ history = await saver.list_checkpoints(thread_id="t1") # walk lineage ``` Notebook walkthrough: [Notebook 07 — Oracle 26ai -checkpointer](../notebooks/notebook_07_oracle_26ai_checkpointer.md). +checkpointer](../notebooks/notebook_53_oracle_26ai_checkpointer.md). ## Two checkpointer shapes — the gotcha to know diff --git a/docs/concepts/deepagent.md b/docs/concepts/deepagent.md index cd230040..ffd555f5 100644 --- a/docs/concepts/deepagent.md +++ b/docs/concepts/deepagent.md @@ -349,7 +349,7 @@ workflow = create_research_workflow( ## See also -- [Notebook 42](../notebooks/notebook_35_deepagent.md) — four-part +- [Notebook 42](../notebooks/notebook_29_deepagent.md) — four-part walkthrough: basic factory, filesystem + todos, subagents, observability. - [API reference — DeepAgent](../api/deepagent.md) — full class and function signatures including `create_research_workflow`. diff --git a/docs/concepts/gsar.md b/docs/concepts/gsar.md index 9f838a4f..9139ef72 100644 --- a/docs/concepts/gsar.md +++ b/docs/concepts/gsar.md @@ -9,7 +9,8 @@ scalar over the answer as a whole — often misses this. Each *claim* is grounded; the *conclusion* over-reaches. **GSAR** (Grounding-Stratified Adaptive Replanning, from -[Kamelhar 2026](https://arxiv.org/abs/2604.23366)) is the upgrade. It +[Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366)) +is the upgrade. It breaks the synthesis into claims, partitions them four ways, scores the partition with per-evidence-type weights, and picks one of three responses: `proceed` if the synthesis holds up, `regenerate` if the @@ -153,6 +154,6 @@ production you'd map your tool taxonomy onto these. — outer loop, abstain handling, budget exhaustion. - [`tests/integration/test_gsar_live.py`](https://github.com/oracle-samples/locus/blob/main/tests/integration/test_gsar_live.py) — live LLM judge driving the full loop. -- [`examples/notebook_43_gsar_typed_grounding.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_43_gsar_typed_grounding.py) +- [`examples/notebook_37_gsar_typed_grounding.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_37_gsar_typed_grounding.py) — a runnable walkthrough of the four parts. -- Paper: [arXiv:2604.23366](https://arxiv.org/abs/2604.23366). +- Paper: [Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366). diff --git a/docs/concepts/hooks.md b/docs/concepts/hooks.md index da34c738..c66a8597 100644 --- a/docs/concepts/hooks.md +++ b/docs/concepts/hooks.md @@ -220,8 +220,8 @@ any field; this is intentionally tight. - [`HookProvider` and `HookOrchestrator`](https://github.com/oracle-samples/locus/blob/main/src/locus/hooks/provider.py) - [Built-in hooks](https://github.com/oracle-samples/locus/tree/main/src/locus/hooks/builtin) -- [`notebook_18_agent_hooks.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_18_agent_hooks.py) — write your first hook. -- [`notebook_20_hooks_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_20_hooks_advanced.py) — guardrails + steering, end to end. +- [`notebook_12_agent_hooks.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_12_agent_hooks.py) — write your first hook. +- [`notebook_14_hooks_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_14_hooks_advanced.py) — guardrails + steering, end to end. ## See also diff --git a/docs/concepts/idempotency.md b/docs/concepts/idempotency.md index 490c7ef3..7d2ddfa9 100644 --- a/docs/concepts/idempotency.md +++ b/docs/concepts/idempotency.md @@ -131,7 +131,7 @@ the side effects still fire exactly once. - [`@tool` decorator with idempotency hook](https://github.com/oracle-samples/locus/blob/main/src/locus/tools/decorator.py) - [`_find_matching_execution`](https://github.com/oracle-samples/locus/blob/main/src/locus/loop/nodes.py#L114) — where the dedup actually happens, in the ReAct loop's Execute node. -- [`notebook_15_agent_with_tools.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_15_agent_with_tools.py) — walks through the `@tool` decorator end-to-end (idempotency covered in the agent-loop walkthrough). +- [`notebook_07_agent_with_tools.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_07_agent_with_tools.py) — walks through the `@tool` decorator end-to-end (idempotency covered in the agent-loop walkthrough). ## See also diff --git a/docs/concepts/interrupts.md b/docs/concepts/interrupts.md index 549ff437..abcbe329 100644 --- a/docs/concepts/interrupts.md +++ b/docs/concepts/interrupts.md @@ -136,9 +136,9 @@ debugging, or branch off a new thread from the partial conversation. - [Hooks](hooks.md) — write custom hooks that return `Cancel`. - [Conversation Management](conversation-management.md) — how `thread_id` resumption works. -- [Notebook 09 — human in the loop](https://github.com/oracle-samples/locus/blob/main/examples/notebook_25_human_in_the_loop.py) +- [Notebook 09 — human in the loop](https://github.com/oracle-samples/locus/blob/main/examples/notebook_19_human_in_the_loop.py) — a full runnable example. -- [Notebook 46 — multi-agent + HITL](https://github.com/oracle-samples/locus/blob/main/examples/notebook_39_multiagent_human_in_loop.py) +- [Notebook 46 — multi-agent + HITL](https://github.com/oracle-samples/locus/blob/main/examples/notebook_33_multiagent_human_in_loop.py) — three HITL patterns in one file (approval gate, human-as-tool, long-pause snapshot/resume). - [Notebook 47 — incident response](https://github.com/oracle-samples/locus/blob/main/examples/notebook_63_incident_response.py) diff --git a/docs/concepts/mcp.md b/docs/concepts/mcp.md index 852c7187..4cb9010b 100644 --- a/docs/concepts/mcp.md +++ b/docs/concepts/mcp.md @@ -199,7 +199,7 @@ implementation detail. ## Source and notebook - [`locus.integrations.fastmcp`](https://github.com/oracle-samples/locus/blob/main/src/locus/integrations/fastmcp.py) — built on FastMCP. -- [`notebook_47_mcp_integration.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_47_mcp_integration.py) — consumer + producer end-to-end. +- [`notebook_45_mcp_integration.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_45_mcp_integration.py) — consumer + producer end-to-end. ## See also diff --git a/docs/concepts/memory-manager.md b/docs/concepts/memory-manager.md index 72198c1c..36b859ab 100644 --- a/docs/concepts/memory-manager.md +++ b/docs/concepts/memory-manager.md @@ -190,7 +190,7 @@ The upstream is currently alpha (`oracleagentmemory` 26.4.0, Dev Status 3). When you need a portable backend or a fully-deterministic LLM-free extractor, fall back to the portable path below. -See [notebook 13](../notebooks/notebook_13_oracle_agent_memory.md) for +See [notebook 13](../notebooks/notebook_09_oracle_agent_memory.md) for a runnable end-to-end demo with two sessions on the same `user_id`. ### Portable path (any BaseStore backend) diff --git a/docs/concepts/multi-agent.md b/docs/concepts/multi-agent.md index 52b4ac7a..07bc9715 100644 --- a/docs/concepts/multi-agent.md +++ b/docs/concepts/multi-agent.md @@ -21,11 +21,11 @@ upgrades to live OCI / OpenAI by setting one env var. | | Workflow | One line | Code | |---|---|---|---| -| **41** | DeepAgent — research factory | `create_deepagent` with reflexion + grounding + subagent dispatch + `deepagent.*` SSE events. | [`notebook_35_deepagent.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_35_deepagent.py) | -| **42** | Map-reduce code review | Scatter a diff to `N` reviewers via `Send`, reduce findings into one report. | [`notebook_36_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_map_reduce_code_review.py) | -| **43** | Supervisor + critic loop | Researcher → Writer → Critic, loop back to Writer until critic approves (cap'd revisions). | [`notebook_37_supervisor_critic_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_37_supervisor_critic_loop.py) | -| **44** | Adversarial debate + judge | PRO and CON argue across N rounds; Judge emits a typed `Verdict` via `output_schema`. | [`notebook_38_debate_with_judge.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_38_debate_with_judge.py) | -| **45** | Multi-agent + human-in-the-loop | Three patterns in one file: approval gate, human-as-tool, long-pause snapshot/resume. | [`notebook_39_multiagent_human_in_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_39_multiagent_human_in_loop.py) | +| **41** | DeepAgent — research factory | `create_deepagent` with reflexion + grounding + subagent dispatch + `deepagent.*` SSE events. | [`notebook_29_deepagent.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_29_deepagent.py) | +| **42** | Map-reduce code review | Scatter a diff to `N` reviewers via `Send`, reduce findings into one report. | [`notebook_30_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_map_reduce_code_review.py) | +| **43** | Supervisor + critic loop | Researcher → Writer → Critic, loop back to Writer until critic approves (cap'd revisions). | [`notebook_31_supervisor_critic_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_31_supervisor_critic_loop.py) | +| **44** | Adversarial debate + judge | PRO and CON argue across N rounds; Judge emits a typed `Verdict` via `output_schema`. | [`notebook_32_debate_with_judge.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_32_debate_with_judge.py) | +| **45** | Multi-agent + human-in-the-loop | Three patterns in one file: approval gate, human-as-tool, long-pause snapshot/resume. | [`notebook_33_multiagent_human_in_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_33_multiagent_human_in_loop.py) | | **46** | On-call incident response | Triage → 3 parallel investigators (logs / metrics / traces) → severity gate → page-the-human → mitigate → typed `Postmortem`. | [`notebook_63_incident_response.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_63_incident_response.py) | | **47** | Tiered approval workflow | Justifier → Vendor analyst → tier router (auto / manager / +finance / +CFO) → typed `PurchaseOrder`. Three stacked `interrupt()` gates on the top tier. | [`notebook_64_procurement_approval.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_64_procurement_approval.py) | | **48** | Contract review + negotiation | Parser → 3 parallel reviewers → negotiation gate → human counsel → `Command(goto="sign_off")` short-circuits when resolved. Cycles enabled. | [`notebook_65_contract_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_65_contract_review.py) | diff --git a/docs/concepts/multi-agent/a2a.md b/docs/concepts/multi-agent/a2a.md index 97be9561..23d1a865 100644 --- a/docs/concepts/multi-agent/a2a.md +++ b/docs/concepts/multi-agent/a2a.md @@ -172,7 +172,7 @@ use `Message` + `client.send_message()` so they can read the full ## Notebook -[`notebook_34_a2a_protocol.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_34_a2a_protocol.py) +[`notebook_28_a2a_protocol.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_28_a2a_protocol.py) — host + client + streaming. ## Source diff --git a/docs/concepts/multi-agent/composition.md b/docs/concepts/multi-agent/composition.md index 9891f03e..f593b326 100644 --- a/docs/concepts/multi-agent/composition.md +++ b/docs/concepts/multi-agent/composition.md @@ -78,12 +78,12 @@ result = end_to_end.run_sync("Brief on Q3 launch.") ## Notebooks -- [`notebook_27_composition.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_27_composition.py) +- [`notebook_21_composition.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_21_composition.py) — `SequentialPipeline`, `ParallelPipeline`, `LoopAgent`. -- [`notebook_36_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_map_reduce_code_review.py) +- [`notebook_30_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_map_reduce_code_review.py) — same fan-out shape with `Send` inside a graph (use this when you need state-aware fan-out beyond what `ParallelPipeline` gives you). -- [`notebook_37_supervisor_critic_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_37_supervisor_critic_loop.py) +- [`notebook_31_supervisor_critic_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_31_supervisor_critic_loop.py) — `LoopAgent`-style refine-until-confidence written as a graph (the cycle version when you also need conditional edges). diff --git a/docs/concepts/multi-agent/functional.md b/docs/concepts/multi-agent/functional.md index d6b96138..3aeb944c 100644 --- a/docs/concepts/multi-agent/functional.md +++ b/docs/concepts/multi-agent/functional.md @@ -102,9 +102,9 @@ async def end_to_end(catalogue: list[dict]) -> dict: ## Notebooks -- [`notebook_29_functional_api.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_29_functional_api.py) +- [`notebook_23_functional_api.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_23_functional_api.py) — `@task` and `@entrypoint` end-to-end. -- [`notebook_36_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_map_reduce_code_review.py) +- [`notebook_30_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_map_reduce_code_review.py) — same map/reduce shape, written as a graph with `Send` instead. Useful as the "graph version" comparison when you're choosing between functional and StateGraph for a fan-out workload. diff --git a/docs/concepts/multi-agent/graph.md b/docs/concepts/multi-agent/graph.md index ac61a67a..db988982 100644 --- a/docs/concepts/multi-agent/graph.md +++ b/docs/concepts/multi-agent/graph.md @@ -119,17 +119,17 @@ a design review. ## Notebooks -- [`notebook_22_basic_graph.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_22_basic_graph.py) +- [`notebook_16_basic_graph.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_16_basic_graph.py) — your first StateGraph. -- [`notebook_23_conditional_routing.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_23_conditional_routing.py) +- [`notebook_17_conditional_routing.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_17_conditional_routing.py) — `add_conditional_edges`. -- [`notebook_24_state_reducers.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_24_state_reducers.py) +- [`notebook_18_state_reducers.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_18_state_reducers.py) — custom state reducers. -- [`notebook_28_graph_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_28_graph_advanced.py) +- [`notebook_22_graph_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_22_graph_advanced.py) — `RetryPolicy`, `CachePolicy`, subgraphs, Mermaid output. -- [`notebook_36_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_map_reduce_code_review.py) +- [`notebook_30_map_reduce_code_review.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_map_reduce_code_review.py) — `Send` fan-out / reduce in a graph. -- [`notebook_37_supervisor_critic_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_37_supervisor_critic_loop.py) +- [`notebook_31_supervisor_critic_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_31_supervisor_critic_loop.py) — `allow_cycles=True` + `max_iterations` for refine-until-confidence. - [`notebook_63_incident_response.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_63_incident_response.py) — triage → parallel investigators → severity gate → page-the-human. diff --git a/docs/concepts/multi-agent/handoff.md b/docs/concepts/multi-agent/handoff.md index f708d418..e577374a 100644 --- a/docs/concepts/multi-agent/handoff.md +++ b/docs/concepts/multi-agent/handoff.md @@ -104,9 +104,9 @@ customer's last message. ## Notebooks -- [`notebook_31_agent_handoff.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_31_agent_handoff.py) +- [`notebook_25_agent_handoff.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_25_agent_handoff.py) — full triage + billing + shipping + returns escalation flow. -- [`notebook_39_multiagent_human_in_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_39_multiagent_human_in_loop.py) +- [`notebook_33_multiagent_human_in_loop.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_33_multiagent_human_in_loop.py) — handoff to a human via `interrupt()` (one of three HITL patterns in the same file). diff --git a/docs/concepts/multi-agent/orchestrator.md b/docs/concepts/multi-agent/orchestrator.md index 9b85f1e0..2a54241a 100644 --- a/docs/concepts/multi-agent/orchestrator.md +++ b/docs/concepts/multi-agent/orchestrator.md @@ -112,9 +112,9 @@ Specialist( ## Notebooks -- [`notebook_32_orchestrator_pattern.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_32_orchestrator_pattern.py) +- [`notebook_26_orchestrator_pattern.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_26_orchestrator_pattern.py) — router + three parallel specialists, results merged. -- [`notebook_33_specialist_agents.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_33_specialist_agents.py) +- [`notebook_27_specialist_agents.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_27_specialist_agents.py) — confidence floors and per-specialist playbooks. - [`notebook_64_procurement_approval.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_64_procurement_approval.py) — orchestrator-shaped procurement with tiered approval gates and a diff --git a/docs/concepts/multi-agent/production.md b/docs/concepts/multi-agent/production.md index 0050f6c9..7184578c 100644 --- a/docs/concepts/multi-agent/production.md +++ b/docs/concepts/multi-agent/production.md @@ -195,7 +195,7 @@ That's the moat. Pick a [shape](../multi-agent.md) directly, or let right one from a typed intent. Then wire the primitives above through it and ship it. -[t44]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_38_debate_with_judge.py +[t44]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_32_debate_with_judge.py [t46]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_63_incident_response.py [t47]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_64_procurement_approval.py [t48]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_65_contract_review.py diff --git a/docs/concepts/multi-agent/swarm.md b/docs/concepts/multi-agent/swarm.md index 463ed92c..64d9ead7 100644 --- a/docs/concepts/multi-agent/swarm.md +++ b/docs/concepts/multi-agent/swarm.md @@ -99,7 +99,7 @@ Swarms stop when: ## Notebook -[`notebook_30_swarm_multiagent.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_swarm_multiagent.py) +[`notebook_24_swarm_multiagent.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_24_swarm_multiagent.py) — a three-agent research swarm with shared context. ## Source diff --git a/docs/concepts/observability.md b/docs/concepts/observability.md index 305a263b..a9183029 100644 --- a/docs/concepts/observability.md +++ b/docs/concepts/observability.md @@ -129,8 +129,8 @@ anything leaves, see [Safety](safety.md). ## Source and notebooks -- [`notebook_18_agent_hooks.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_18_agent_hooks.py) — first hook, including logging. -- [`notebook_20_hooks_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_20_hooks_advanced.py) — telemetry pipelines. +- [`notebook_12_agent_hooks.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_12_agent_hooks.py) — first hook, including logging. +- [`notebook_14_hooks_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_14_hooks_advanced.py) — telemetry pipelines. - [`locus.hooks.builtin.logging`](https://github.com/oracle-samples/locus/blob/main/src/locus/hooks/builtin/logging.py) — `LoggingHook`, `StructuredLoggingHook`. - [`locus.hooks.builtin.telemetry`](https://github.com/oracle-samples/locus/blob/main/src/locus/hooks/builtin/telemetry.py) — `TelemetryHook`, `NoOpTelemetryHook`. diff --git a/docs/concepts/playbooks.md b/docs/concepts/playbooks.md index 9916c042..24e1f2dc 100644 --- a/docs/concepts/playbooks.md +++ b/docs/concepts/playbooks.md @@ -192,7 +192,7 @@ for execution in plan.executions: ## Source and notebook -- [`notebook_48_playbooks.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_48_playbooks.py) — runnable end-to-end with execution tracking. +- [`notebook_46_playbooks.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_46_playbooks.py) — runnable end-to-end with execution tracking. - [`locus.playbooks`](https://github.com/oracle-samples/locus/tree/main/src/locus/playbooks) — `Playbook`, `PlaybookStep`, `PlaybookEnforcerHook`, `load_playbook`. ## See also diff --git a/docs/concepts/rag.md b/docs/concepts/rag.md index 64fb58ac..1477c327 100644 --- a/docs/concepts/rag.md +++ b/docs/concepts/rag.md @@ -302,9 +302,9 @@ re-ranking before they reach the agent. ## Source and notebooks -- [`notebook_44_rag_basics.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_44_rag_basics.py) — minimal end-to-end RAG. -- [`notebook_45_rag_providers.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_45_rag_providers.py) — picking an embedder + store. -- [`notebook_46_rag_agents.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_46_rag_agents.py) — `create_rag_tool` plugged into an agent. +- [`notebook_38_rag_basics.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_38_rag_basics.py) — minimal end-to-end RAG. +- [`notebook_39_rag_providers.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_39_rag_providers.py) — picking an embedder + store. +- [`notebook_40_rag_agents.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_40_rag_agents.py) — `create_rag_tool` plugged into an agent. - [`locus.rag`](https://github.com/oracle-samples/locus/tree/main/src/locus/rag) — `RAGRetriever`, all embedders, all stores, `create_rag_tool`, `RAGToolkit`. ## See also diff --git a/docs/concepts/reasoning.md b/docs/concepts/reasoning.md index b27be5fe..b865fecf 100644 --- a/docs/concepts/reasoning.md +++ b/docs/concepts/reasoning.md @@ -128,7 +128,7 @@ observable as their own event types. ## Source and notebook -- [`notebook_42_reasoning_patterns.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_42_reasoning_patterns.py) — all three add-ons end-to-end. +- [`notebook_36_reasoning_patterns.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_reasoning_patterns.py) — all three add-ons end-to-end. - [`locus.reasoning.reflexion`](https://github.com/oracle-samples/locus/blob/main/src/locus/reasoning/reflexion.py) - [`locus.reasoning.grounding`](https://github.com/oracle-samples/locus/blob/main/src/locus/reasoning/grounding.py) - [`locus.reasoning.causal`](https://github.com/oracle-samples/locus/blob/main/src/locus/reasoning/causal.py) diff --git a/docs/concepts/router.md b/docs/concepts/router.md index 64b87ee7..15adc5f8 100644 --- a/docs/concepts/router.md +++ b/docs/concepts/router.md @@ -223,7 +223,7 @@ identifying which path produced the pick: | `"llm_picked"` | Picker resolved the disambiguation; `rationale` field populated | | `"rule_based_fallback"` | Picker raised or hallucinated; `_rank_key` resolved it | -See [notebook 59](../notebooks/notebook_40_emergent_routing.md) for a +See [notebook 59](../notebooks/notebook_34_emergent_routing.md) for a runnable side-by-side comparison. ## Skills integration diff --git a/docs/concepts/safety.md b/docs/concepts/safety.md index d8c609cb..1a856b41 100644 --- a/docs/concepts/safety.md +++ b/docs/concepts/safety.md @@ -150,9 +150,9 @@ runs; the model sees the typed-error message and retries with `"C"`. ## Source and notebooks -- [`notebook_52_guardrails_security.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_52_guardrails_security.py) — basic guardrails. -- [`notebook_53_guardrails_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_53_guardrails_advanced.py) — topic + content + PII layered. -- [`notebook_51_steering.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_51_steering.py) — judge-model approval. +- [`notebook_50_guardrails_security.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_50_guardrails_security.py) — basic guardrails. +- [`notebook_51_guardrails_advanced.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_51_guardrails_advanced.py) — topic + content + PII layered. +- [`notebook_49_steering.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_49_steering.py) — judge-model approval. - [`locus.hooks.builtin.guardrails`](https://github.com/oracle-samples/locus/blob/main/src/locus/hooks/builtin/guardrails.py) - [`locus.hooks.builtin.steering`](https://github.com/oracle-samples/locus/blob/main/src/locus/hooks/builtin/steering.py) diff --git a/docs/concepts/skills.md b/docs/concepts/skills.md index 70e94954..c7be5b89 100644 --- a/docs/concepts/skills.md +++ b/docs/concepts/skills.md @@ -164,7 +164,7 @@ of them call. ## Source and notebook -- [`notebook_50_skills.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_50_skills.py) — programmatic and filesystem-loaded skills end-to-end. +- [`notebook_48_skills.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_48_skills.py) — programmatic and filesystem-loaded skills end-to-end. - [`locus.skills`](https://github.com/oracle-samples/locus/tree/main/src/locus/skills) — `Skill`, `SkillsPlugin`. - [AgentSkills.io specification](https://agentskills.io) — the format locus implements. diff --git a/docs/concepts/streaming.md b/docs/concepts/streaming.md index 8ae65e40..a4a0c2c0 100644 --- a/docs/concepts/streaming.md +++ b/docs/concepts/streaming.md @@ -171,8 +171,8 @@ es.addEventListener('ModelChunkEvent', (e) => { ## Notebooks -- [`notebook_17_agent_streaming.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_17_agent_streaming.py) — your first event consumer. -- [`notebook_19_sse_streaming.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_19_sse_streaming.py) — full SSE wiring against `AgentServer`. +- [`notebook_11_agent_streaming.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_11_agent_streaming.py) — your first event consumer. +- [`notebook_13_sse_streaming.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_13_sse_streaming.py) — full SSE wiring against `AgentServer`. ## Source diff --git a/docs/concepts/structured-output.md b/docs/concepts/structured-output.md index 35fa7dee..97c4ac4d 100644 --- a/docs/concepts/structured-output.md +++ b/docs/concepts/structured-output.md @@ -158,10 +158,10 @@ builder, validation-error formatter. ## Notebooks -- [`notebook_41_structured_output.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_41_structured_output.py) +- [`notebook_35_structured_output.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_35_structured_output.py) covers both the standalone `parse_structured()` parser (useful for non-Agent flows) and the Agent `output_schema=` integration above. -- [`notebook_38_debate_with_judge.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_38_debate_with_judge.py) +- [`notebook_32_debate_with_judge.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_32_debate_with_judge.py) — typed `Verdict` as the workflow boundary artifact. - [`notebook_63_incident_response.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_63_incident_response.py) — typed `Postmortem` as the terminal artifact of an incident graph. diff --git a/docs/concepts/termination.md b/docs/concepts/termination.md index faf93bfa..9aa659d6 100644 --- a/docs/concepts/termination.md +++ b/docs/concepts/termination.md @@ -128,7 +128,7 @@ and `|` work across the whole hierarchy. ## Source and notebook -- [`notebook_21_termination.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_21_termination.py) — runnable algebra examples. +- [`notebook_15_termination.py`](https://github.com/oracle-samples/locus/blob/main/examples/notebook_15_termination.py) — runnable algebra examples. - [`locus.core.termination`](https://github.com/oracle-samples/locus/blob/main/src/locus/core/termination.py) — every condition class, plus `__or__` / `__and__`. ## See also diff --git a/docs/concepts/tools.md b/docs/concepts/tools.md index 2185ceab..485fcd21 100644 --- a/docs/concepts/tools.md +++ b/docs/concepts/tools.md @@ -181,6 +181,65 @@ If you've built a tool you want other agents to reach, expose it through `LocusMCPServer` — same `@tool`, no rewrite. See [MCP](mcp.md). +## Open-spec built-ins for the Oracle estate + +For most domains, you write a `@tool` per operation: `search_orders`, +`get_invoice`, `cancel_subscription`. That stops scaling once the +domain is *cloud-sized* — the Oracle Cloud Infrastructure (OCI) +Python SDK alone has ~190 service modules. Locus ships two built-in +**open-specification** tools that cover the whole surface through a +single agentic primitive: + +```python +from locus.tools import describe_oci, use_oci + +agent = Agent( + model=get_model("oci:openai.gpt-5.5", profile="MY_GENAI"), + tools=[describe_oci, use_oci], + system_prompt=""" +You can call OCI. When unsure of the right service / client / +operation / parameters, call describe_oci first — then use_oci. +""", +) + +agent.run_sync("Are there any compute instances running anywhere in my tenancy?") +``` + +- **`describe_oci(service?, client?, operation?)`** — runtime + introspection. Progressively zooms: no args → list ~190 services; + `service` → list `*Client` classes; `service + client` → list + operations partitioned read-only vs mutating; `service + client + + operation` → parameter schema parsed from the OCI SDK `:param:` + docstring. +- **`use_oci(service, client, operation, parameters, ...)`** — + execute. Builds the right `oci..` with the + requested auth (`api_key`, `security_token`, `instance_principal`, + `resource_principal`), invokes the method, returns the serialised + response with `http_status` + `opc-request-id` for traceability. + +This is the agentic-framework angle on cloud control planes: +**hand the model two tools, not 190.** The model uses `describe_oci` +like a `man` page, then dispatches `use_oci`. One execution path, one +auth code-path, one serialiser, one mutation-safety gate covers all +of Identity, Compute, Database, Object Storage, Networking, Vault, +Functions, Bastion, GenAI, and the rest. + +`use_oci` is **read-only by default** — operations that don't start +with a read-only verb (`list_/get_/head_/summarize_/describe_/ +search_/fetch_/compute_/preview_/validate_/test_`) are refused +unless the caller passes `allow_mutations=True` or sets +`LOCUS_USE_OCI_ALLOW_MUTATIONS=1`. Enforced in code, not via +interactive prompt, because agent tools must be non-interactive. + +The same open-spec pattern composes with Oracle's own data-plane +primitives. If your agent needs to **read or write data** (not just +operate the control plane), wire it up with the Oracle 26ai RAG + +in-DB primitives — see [Notebook 41 — Oracle 26ai +RAG](../notebooks/notebook_41_oracle_26ai_rag.md) and the +[`docs/concepts/rag.md`](rag.md) chapter. The full agent-driven OCI +walkthrough is in [Notebook 70 — `use_oci` + +`describe_oci`](../notebooks/notebook_70_oci_tools.md). + ## Common gotchas | Symptom | Likely cause | @@ -195,7 +254,7 @@ through `LocusMCPServer` — same `@tool`, no rewrite. See - [`@tool` decorator and `Tool` class](https://github.com/oracle-samples/locus/blob/main/src/locus/tools/decorator.py) - [`ToolRegistry`](https://github.com/oracle-samples/locus/blob/main/src/locus/tools/registry.py) -- [Built-in tools](https://github.com/oracle-samples/locus/tree/main/src/locus/tools/builtins) — `get_today_date`, `task_complete`, `ask_user` +- [Built-in tools](https://github.com/oracle-samples/locus/tree/main/src/locus/tools/builtins) — `get_today_date`, `task_complete`, `ask_user`, `use_oci`, `describe_oci` ## See also diff --git a/docs/how-to/environment-variables.md b/docs/how-to/environment-variables.md index 7fd121fe..946892f0 100644 --- a/docs/how-to/environment-variables.md +++ b/docs/how-to/environment-variables.md @@ -42,7 +42,7 @@ export OCI_PROFILE=DEFAULT export OCI_REGION=us-chicago-1 # Run any notebook: -python examples/notebook_14_basic_agent.py +python examples/notebook_06_basic_agent.py ``` When the SDK itself is constructed directly (not via the notebook diff --git a/docs/how-to/oci-dac.md b/docs/how-to/oci-dac.md index 69739a3a..764b46ee 100644 --- a/docs/how-to/oci-dac.md +++ b/docs/how-to/oci-dac.md @@ -123,7 +123,7 @@ export LOCUS_MODEL_PROVIDER=oci export LOCUS_MODEL_ID="ocid1.generativeaiendpoint.oc1....." export LOCUS_OCI_PROFILE=MY_PROFILE export LOCUS_OCI_COMPARTMENT="ocid1.compartment.oc1..." -python examples/notebook_14_basic_agent.py +python examples/notebook_06_basic_agent.py ``` `LOCUS_OCI_TRANSPORT=sdk` forces the SDK transport explicitly if you diff --git a/docs/index.md b/docs/index.md index c5c0914d..bdcffd2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -105,7 +105,7 @@ agent = Agent( print(agent.run_sync("Should I bring an umbrella to Tokyo tomorrow?").text) ``` -[Notebook 14 — basic agent →](notebooks/notebook_14_basic_agent.md) +[Notebook 14 — basic agent →](notebooks/notebook_06_basic_agent.md) ## What locus gives you @@ -218,13 +218,13 @@ the source from there. | What | Notebooks (click any one) | |---|---| -| Agent + ReAct loop | [13 — Basic agent](notebooks/notebook_14_basic_agent.md) · [14 — Agent with tools](notebooks/notebook_15_agent_with_tools.md) · [17 — Lifecycle hooks](notebooks/notebook_18_agent_hooks.md) | -| Cognitive router (PRISM) | [57 — Cognitive router](notebooks/notebook_58_cognitive_router.md) · [39 — Emergent routing](notebooks/notebook_40_emergent_routing.md) | -| Multi-agent shapes | [21 — Basic graph](notebooks/notebook_22_basic_graph.md) · [26 — Composition](notebooks/notebook_27_composition.md) · [29 — Swarm](notebooks/notebook_30_swarm_multiagent.md) · [30 — Handoff](notebooks/notebook_31_agent_handoff.md) · [31 — Orchestrator](notebooks/notebook_32_orchestrator_pattern.md) · [33 — A2A](notebooks/notebook_34_a2a_protocol.md) | -| Observability | [16 — Streaming events](notebooks/notebook_17_agent_streaming.md) · [58 — Observability basics](notebooks/notebook_59_observability_basics.md) · [61 — Event catalogue](notebooks/notebook_62_event_catalogue.md) | -| Idempotent tools · termination | [14 — Agent with tools](notebooks/notebook_15_agent_with_tools.md) · [20 — Termination](notebooks/notebook_21_termination.md) | +| Agent + ReAct loop | [13 — Basic agent](notebooks/notebook_06_basic_agent.md) · [14 — Agent with tools](notebooks/notebook_07_agent_with_tools.md) · [17 — Lifecycle hooks](notebooks/notebook_12_agent_hooks.md) | +| Cognitive router (PRISM) | [57 — Cognitive router](notebooks/notebook_58_cognitive_router.md) · [39 — Emergent routing](notebooks/notebook_34_emergent_routing.md) | +| Multi-agent shapes | [21 — Basic graph](notebooks/notebook_16_basic_graph.md) · [26 — Composition](notebooks/notebook_21_composition.md) · [29 — Swarm](notebooks/notebook_24_swarm_multiagent.md) · [30 — Handoff](notebooks/notebook_25_agent_handoff.md) · [31 — Orchestrator](notebooks/notebook_26_orchestrator_pattern.md) · [33 — A2A](notebooks/notebook_28_a2a_protocol.md) | +| Observability | [16 — Streaming events](notebooks/notebook_11_agent_streaming.md) · [58 — Observability basics](notebooks/notebook_59_observability_basics.md) · [61 — Event catalogue](notebooks/notebook_62_event_catalogue.md) | +| Idempotent tools · termination | [14 — Agent with tools](notebooks/notebook_07_agent_with_tools.md) · [20 — Termination](notebooks/notebook_15_termination.md) | | OCI model transport | [01 — OCI transports](notebooks/notebook_01_oci_transports.md) · [02 — OCI v1](notebooks/notebook_02_oci_openai_chat.md) · [03 — OCI Responses](notebooks/notebook_03_oci_responses.md) · [04 — Dedicated AI Cluster](notebooks/notebook_04_oci_dac.md) | -| Oracle 26ai primitives | [06 — RAG](notebooks/notebook_06_oracle_26ai_rag.md) · [07 — Checkpointer](notebooks/notebook_07_oracle_26ai_checkpointer.md) · [08 — ADB loader](notebooks/notebook_08_oracle_adb_loader.md) · [09 — In-DB chunker](notebooks/notebook_09_oracle_indb_chunker.md) · [10 — In-DB embeddings](notebooks/notebook_10_oracle_indb_embeddings.md) · [11 — Cross-thread store](notebooks/notebook_11_oracle_store.md) · [12 — Versioned saver](notebooks/notebook_12_oracle_versioned_saver.md) | +| Oracle 26ai primitives | [06 — RAG](notebooks/notebook_41_oracle_26ai_rag.md) · [07 — Checkpointer](notebooks/notebook_53_oracle_26ai_checkpointer.md) · [08 — ADB loader](notebooks/notebook_42_oracle_adb_loader.md) · [09 — In-DB chunker](notebooks/notebook_43_oracle_indb_chunker.md) · [10 — In-DB embeddings](notebooks/notebook_44_oracle_indb_embeddings.md) · [11 — Cross-thread store](notebooks/notebook_10_oracle_store.md) · [12 — Versioned saver](notebooks/notebook_54_oracle_versioned_saver.md) | Full catalog → [Notebooks index](notebooks/index.md) · [Capabilities matrix](capabilities.md) · [API reference](api/agent.md) diff --git a/docs/notebooks/index.md b/docs/notebooks/index.md index c22a9923..de23c0d9 100644 --- a/docs/notebooks/index.md +++ b/docs/notebooks/index.md @@ -178,49 +178,49 @@ End-to-end use cases — incident response, contract review, audio chat. [t03]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_03_oci_responses.py [t04]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_04_oci_dac.py [t05]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_05_cohere_reranker.py -[t06]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_06_oracle_26ai_rag.py -[t07]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_07_oracle_26ai_checkpointer.py -[t08]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_14_basic_agent.py -[t09]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_15_agent_with_tools.py -[t10]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_16_agent_memory.py -[t11]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_17_agent_streaming.py -[t12]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_18_agent_hooks.py -[t13]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_19_sse_streaming.py -[t14]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_20_hooks_advanced.py -[t15]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_21_termination.py -[t16]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_22_basic_graph.py -[t17]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_23_conditional_routing.py -[t18]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_24_state_reducers.py -[t19]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_25_human_in_the_loop.py -[t20]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_26_advanced_patterns.py -[t21]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_27_composition.py -[t22]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_28_graph_advanced.py -[t23]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_29_functional_api.py -[t24]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_swarm_multiagent.py -[t25]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_31_agent_handoff.py -[t26]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_32_orchestrator_pattern.py -[t27]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_33_specialist_agents.py -[t28]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_34_a2a_protocol.py -[t29]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_35_deepagent.py -[t30]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_map_reduce_code_review.py -[t31]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_37_supervisor_critic_loop.py -[t32]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_38_debate_with_judge.py -[t33]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_39_multiagent_human_in_loop.py -[t34]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_40_emergent_routing.py -[t35]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_41_structured_output.py -[t36]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_42_reasoning_patterns.py -[t37]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_43_gsar_typed_grounding.py -[t38]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_44_rag_basics.py -[t39]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_45_rag_providers.py -[t40]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_46_rag_agents.py -[t41]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_47_mcp_integration.py -[t42]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_48_playbooks.py -[t43]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_49_plugins.py -[t44]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_50_skills.py -[t45]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_51_steering.py -[t46]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_52_guardrails_security.py -[t47]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_53_guardrails_advanced.py -[t48]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_54_checkpoint_backends.py +[t06]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_41_oracle_26ai_rag.py +[t07]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_53_oracle_26ai_checkpointer.py +[t08]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_06_basic_agent.py +[t09]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_07_agent_with_tools.py +[t10]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_08_agent_memory.py +[t11]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_11_agent_streaming.py +[t12]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_12_agent_hooks.py +[t13]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_13_sse_streaming.py +[t14]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_14_hooks_advanced.py +[t15]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_15_termination.py +[t16]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_16_basic_graph.py +[t17]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_17_conditional_routing.py +[t18]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_18_state_reducers.py +[t19]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_19_human_in_the_loop.py +[t20]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_20_advanced_patterns.py +[t21]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_21_composition.py +[t22]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_22_graph_advanced.py +[t23]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_23_functional_api.py +[t24]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_24_swarm_multiagent.py +[t25]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_25_agent_handoff.py +[t26]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_26_orchestrator_pattern.py +[t27]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_27_specialist_agents.py +[t28]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_28_a2a_protocol.py +[t29]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_29_deepagent.py +[t30]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_30_map_reduce_code_review.py +[t31]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_31_supervisor_critic_loop.py +[t32]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_32_debate_with_judge.py +[t33]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_33_multiagent_human_in_loop.py +[t34]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_34_emergent_routing.py +[t35]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_35_structured_output.py +[t36]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_36_reasoning_patterns.py +[t37]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_37_gsar_typed_grounding.py +[t38]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_38_rag_basics.py +[t39]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_39_rag_providers.py +[t40]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_40_rag_agents.py +[t41]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_45_mcp_integration.py +[t42]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_46_playbooks.py +[t43]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_47_plugins.py +[t44]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_48_skills.py +[t45]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_49_steering.py +[t46]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_50_guardrails_security.py +[t47]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_51_guardrails_advanced.py +[t48]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_52_checkpoint_backends.py [t49]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_55_evaluation.py [t50]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_56_model_providers.py [t51]: https://github.com/oracle-samples/locus/blob/main/examples/notebook_57_multimodal_providers.py diff --git a/docs/notebooks/notebook_05_cohere_reranker.md b/docs/notebooks/notebook_05_cohere_reranker.md index b71fffc9..2813280e 100644 --- a/docs/notebooks/notebook_05_cohere_reranker.md +++ b/docs/notebooks/notebook_05_cohere_reranker.md @@ -44,7 +44,7 @@ python examples/notebook_05_cohere_reranker.py ## See also -- [Notebook 05 — Oracle 26ai RAG (the store this notebook builds on)](notebook_06_oracle_26ai_rag.md) +- [Notebook 05 — Oracle 26ai RAG (the store this notebook builds on)](notebook_41_oracle_26ai_rag.md) - [Concepts — RAG](../concepts/rag.md) - [OCI Generative AI — documentation hub](https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm) - [Oracle AI Database 26ai — AI Vector Search overview](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/overview-ai-vector-search.html) diff --git a/docs/notebooks/notebook_14_basic_agent.md b/docs/notebooks/notebook_06_basic_agent.md similarity index 88% rename from docs/notebooks/notebook_14_basic_agent.md rename to docs/notebooks/notebook_06_basic_agent.md index 1bf42527..6773ac52 100644 --- a/docs/notebooks/notebook_14_basic_agent.md +++ b/docs/notebooks/notebook_06_basic_agent.md @@ -15,7 +15,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_14_basic_agent.py +.venv/bin/python examples/notebook_06_basic_agent.py ``` The default provider is OCI Generative AI — a working `~/.oci/config` @@ -26,5 +26,5 @@ OpenAI, Anthropic, and Ollama are also supported. ## Source ```python ---8<-- "examples/notebook_14_basic_agent.py" +--8<-- "examples/notebook_06_basic_agent.py" ``` diff --git a/docs/notebooks/notebook_15_agent_with_tools.md b/docs/notebooks/notebook_07_agent_with_tools.md similarity index 87% rename from docs/notebooks/notebook_15_agent_with_tools.md rename to docs/notebooks/notebook_07_agent_with_tools.md index 5cffedca..0482ebd3 100644 --- a/docs/notebooks/notebook_15_agent_with_tools.md +++ b/docs/notebooks/notebook_07_agent_with_tools.md @@ -15,7 +15,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_15_agent_with_tools.py +.venv/bin/python examples/notebook_07_agent_with_tools.py ``` Uses the OCI Generative AI default provider (canonical model id: @@ -28,5 +28,5 @@ Prerequisite: notebook 08. ## Source ```python ---8<-- "examples/notebook_15_agent_with_tools.py" +--8<-- "examples/notebook_07_agent_with_tools.py" ``` diff --git a/docs/notebooks/notebook_16_agent_memory.md b/docs/notebooks/notebook_08_agent_memory.md similarity index 92% rename from docs/notebooks/notebook_16_agent_memory.md rename to docs/notebooks/notebook_08_agent_memory.md index 0b023a01..6d7250cf 100644 --- a/docs/notebooks/notebook_16_agent_memory.md +++ b/docs/notebooks/notebook_08_agent_memory.md @@ -25,7 +25,7 @@ export ORACLE_USER=locus_app export ORACLE_PASSWORD='' export ORACLE_WALLET=~/.oci/wallets/mydb export ORACLE_WALLET_PASSWORD='' # if encrypted -.venv/bin/python examples/notebook_16_agent_memory.py +.venv/bin/python examples/notebook_08_agent_memory.py ``` If those env vars are unset the script prints a skip banner and exits @@ -38,5 +38,5 @@ the LLM. ## Source ```python ---8<-- "examples/notebook_16_agent_memory.py" +--8<-- "examples/notebook_08_agent_memory.py" ``` diff --git a/docs/notebooks/notebook_13_oracle_agent_memory.md b/docs/notebooks/notebook_09_oracle_agent_memory.md similarity index 93% rename from docs/notebooks/notebook_13_oracle_agent_memory.md rename to docs/notebooks/notebook_09_oracle_agent_memory.md index 02fd0310..d0fedf0d 100644 --- a/docs/notebooks/notebook_13_oracle_agent_memory.md +++ b/docs/notebooks/notebook_09_oracle_agent_memory.md @@ -5,7 +5,7 @@ Wires Locus's [`OracleAgentMemoryManager`](../api/memory.md) to [`oracleagentmemory`](https://pypi.org/project/oracleagentmemory/) — Oracle's official AI-agent-memory client backed by Oracle AI Database. -Compared with [notebook 11's](notebook_11_oracle_store.md) +Compared with [notebook 11's](notebook_10_oracle_store.md) `LLMMemoryManager + OracleStore` (which is the portable path that works against any `BaseStore` — InMemory, Redis, Postgres, OpenSearch, Oracle), this notebook brings in: @@ -90,7 +90,7 @@ manager raises a clear `ImportError` pointing at the extras command. ## Run ```bash -python examples/notebook_13_oracle_agent_memory.py +python examples/notebook_09_oracle_agent_memory.py ``` ## Schema hygiene @@ -103,8 +103,8 @@ the runtime user can run with DML-only privileges. ## See also -- [Notebook 11 — Oracle 26ai cross-thread memory agent (portable BaseStore path)](notebook_11_oracle_store.md) -- [Notebook 07 — Oracle 26ai checkpointer](notebook_07_oracle_26ai_checkpointer.md) +- [Notebook 11 — Oracle 26ai cross-thread memory agent (portable BaseStore path)](notebook_10_oracle_store.md) +- [Notebook 07 — Oracle 26ai checkpointer](notebook_53_oracle_26ai_checkpointer.md) - [Concepts → Memory manager](../concepts/memory-manager.md) - [`oracleagentmemory` on PyPI](https://pypi.org/project/oracleagentmemory/) - [Oracle AI Agent Memory — official docs](https://docs.oracle.com/en/database/oracle/agent-memory/index.html) @@ -112,5 +112,5 @@ the runtime user can run with DML-only privileges. ## Source ```python ---8<-- "examples/notebook_13_oracle_agent_memory.py" +--8<-- "examples/notebook_09_oracle_agent_memory.py" ``` diff --git a/docs/notebooks/notebook_11_oracle_store.md b/docs/notebooks/notebook_10_oracle_store.md similarity index 85% rename from docs/notebooks/notebook_11_oracle_store.md rename to docs/notebooks/notebook_10_oracle_store.md index 0ab95c89..8dd9bac2 100644 --- a/docs/notebooks/notebook_11_oracle_store.md +++ b/docs/notebooks/notebook_10_oracle_store.md @@ -1,7 +1,7 @@ # Oracle 26ai cross-thread memory agent A Locus `Agent` with cross-thread long-term memory backed by -`OracleStore`. Where the [Oracle 26ai checkpointer](notebook_07_oracle_26ai_checkpointer.md) +`OracleStore`. Where the [Oracle 26ai checkpointer](notebook_53_oracle_26ai_checkpointer.md) persists *per-thread* state, `OracleStore` persists *cross-thread* facts. The Locus agent reaches into it through `LLMMemoryManager`, which extracts memories at the end of a session @@ -25,9 +25,9 @@ without ever seeing thread A's messages. ## Prerequisites -The store, the [checkpointer](notebook_07_oracle_26ai_checkpointer.md), -the [versioned saver](notebook_12_oracle_versioned_saver.md), and the -[vector store](notebook_06_oracle_26ai_rag.md) can all share a single +The store, the [checkpointer](notebook_53_oracle_26ai_checkpointer.md), +the [versioned saver](notebook_54_oracle_versioned_saver.md), and the +[vector store](notebook_41_oracle_26ai_rag.md) can all share a single Autonomous Database: ```bash @@ -45,7 +45,7 @@ state. ## Run ```bash -python examples/notebook_11_oracle_store.py +python examples/notebook_10_oracle_store.py ``` ## Schema hygiene @@ -59,8 +59,8 @@ only. ## See also -- [Notebook 07 — Oracle 26ai checkpointer](notebook_07_oracle_26ai_checkpointer.md) -- [Notebook 12 — Oracle 26ai versioned checkpoint saver](notebook_12_oracle_versioned_saver.md) +- [Notebook 07 — Oracle 26ai checkpointer](notebook_53_oracle_26ai_checkpointer.md) +- [Notebook 12 — Oracle 26ai versioned checkpoint saver](notebook_54_oracle_versioned_saver.md) - [Oracle Database 26ai — documentation](https://docs.oracle.com/en/database/oracle/oracle-database/23/index.html) - [Oracle AI Database 26ai — AI Vector Search User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/index.html) - [python-oracledb driver](https://python-oracledb.readthedocs.io/en/latest/index.html) @@ -68,5 +68,5 @@ only. ## Source ```python ---8<-- "examples/notebook_11_oracle_store.py" +--8<-- "examples/notebook_10_oracle_store.py" ``` diff --git a/docs/notebooks/notebook_17_agent_streaming.md b/docs/notebooks/notebook_11_agent_streaming.md similarity index 89% rename from docs/notebooks/notebook_17_agent_streaming.md rename to docs/notebooks/notebook_11_agent_streaming.md index 1a4d6698..274cca82 100644 --- a/docs/notebooks/notebook_17_agent_streaming.md +++ b/docs/notebooks/notebook_11_agent_streaming.md @@ -17,7 +17,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_17_agent_streaming.py +.venv/bin/python examples/notebook_11_agent_streaming.py ``` The default provider is OCI Generative AI (canonical id: @@ -29,5 +29,5 @@ Prerequisite: notebook 09. ## Source ```python ---8<-- "examples/notebook_17_agent_streaming.py" +--8<-- "examples/notebook_11_agent_streaming.py" ``` diff --git a/docs/notebooks/notebook_18_agent_hooks.md b/docs/notebooks/notebook_12_agent_hooks.md similarity index 90% rename from docs/notebooks/notebook_18_agent_hooks.md rename to docs/notebooks/notebook_12_agent_hooks.md index f6a208fb..dd9cc9e8 100644 --- a/docs/notebooks/notebook_18_agent_hooks.md +++ b/docs/notebooks/notebook_12_agent_hooks.md @@ -18,7 +18,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_18_agent_hooks.py +.venv/bin/python examples/notebook_12_agent_hooks.py ``` Uses the OCI Generative AI default provider (canonical id: @@ -30,5 +30,5 @@ Prerequisite: notebook 11. ## Source ```python ---8<-- "examples/notebook_18_agent_hooks.py" +--8<-- "examples/notebook_12_agent_hooks.py" ``` diff --git a/docs/notebooks/notebook_19_sse_streaming.md b/docs/notebooks/notebook_13_sse_streaming.md similarity index 89% rename from docs/notebooks/notebook_19_sse_streaming.md rename to docs/notebooks/notebook_13_sse_streaming.md index ada1924a..5b92fb8f 100644 --- a/docs/notebooks/notebook_19_sse_streaming.md +++ b/docs/notebooks/notebook_13_sse_streaming.md @@ -18,7 +18,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_19_sse_streaming.py +.venv/bin/python examples/notebook_13_sse_streaming.py ``` This notebook exercises only the SSE plumbing — no LLM call is made, so @@ -28,5 +28,5 @@ if you want a uniform offline setup across notebooks. ## Source ```python ---8<-- "examples/notebook_19_sse_streaming.py" +--8<-- "examples/notebook_13_sse_streaming.py" ``` diff --git a/docs/notebooks/notebook_20_hooks_advanced.md b/docs/notebooks/notebook_14_hooks_advanced.md similarity index 90% rename from docs/notebooks/notebook_20_hooks_advanced.md rename to docs/notebooks/notebook_14_hooks_advanced.md index 1097b921..eee37438 100644 --- a/docs/notebooks/notebook_20_hooks_advanced.md +++ b/docs/notebooks/notebook_14_hooks_advanced.md @@ -19,7 +19,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_20_hooks_advanced.py +.venv/bin/python examples/notebook_14_hooks_advanced.py ``` Uses the OCI Generative AI default provider (canonical id: @@ -31,5 +31,5 @@ Prerequisite: notebook 12. ## Source ```python ---8<-- "examples/notebook_20_hooks_advanced.py" +--8<-- "examples/notebook_14_hooks_advanced.py" ``` diff --git a/docs/notebooks/notebook_21_termination.md b/docs/notebooks/notebook_15_termination.md similarity index 91% rename from docs/notebooks/notebook_21_termination.md rename to docs/notebooks/notebook_15_termination.md index fd895d45..ee1598a9 100644 --- a/docs/notebooks/notebook_21_termination.md +++ b/docs/notebooks/notebook_15_termination.md @@ -20,7 +20,7 @@ What you'll learn: Run it: ``` -.venv/bin/python examples/notebook_21_termination.py +.venv/bin/python examples/notebook_15_termination.py ``` Uses the OCI Generative AI default provider (canonical id: @@ -30,5 +30,5 @@ Uses the OCI Generative AI default provider (canonical id: ## Source ```python ---8<-- "examples/notebook_21_termination.py" +--8<-- "examples/notebook_15_termination.py" ``` diff --git a/docs/notebooks/notebook_22_basic_graph.md b/docs/notebooks/notebook_16_basic_graph.md similarity index 82% rename from docs/notebooks/notebook_22_basic_graph.md rename to docs/notebooks/notebook_16_basic_graph.md index c6b1aa60..b1b29587 100644 --- a/docs/notebooks/notebook_22_basic_graph.md +++ b/docs/notebooks/notebook_16_basic_graph.md @@ -17,13 +17,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_22_basic_graph.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_16_basic_graph.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_22_basic_graph.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_16_basic_graph.py ``` ## Source ```python ---8<-- "examples/notebook_22_basic_graph.py" +--8<-- "examples/notebook_16_basic_graph.py" ``` diff --git a/docs/notebooks/notebook_23_conditional_routing.md b/docs/notebooks/notebook_17_conditional_routing.md similarity index 80% rename from docs/notebooks/notebook_23_conditional_routing.md rename to docs/notebooks/notebook_17_conditional_routing.md index 0ac411f2..d0fdebe0 100644 --- a/docs/notebooks/notebook_23_conditional_routing.md +++ b/docs/notebooks/notebook_17_conditional_routing.md @@ -17,13 +17,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_23_conditional_routing.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_17_conditional_routing.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_23_conditional_routing.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_17_conditional_routing.py ``` ## Source ```python ---8<-- "examples/notebook_23_conditional_routing.py" +--8<-- "examples/notebook_17_conditional_routing.py" ``` diff --git a/docs/notebooks/notebook_24_state_reducers.md b/docs/notebooks/notebook_18_state_reducers.md similarity index 83% rename from docs/notebooks/notebook_24_state_reducers.md rename to docs/notebooks/notebook_18_state_reducers.md index 5f2fc32e..eb19f39f 100644 --- a/docs/notebooks/notebook_24_state_reducers.md +++ b/docs/notebooks/notebook_18_state_reducers.md @@ -18,13 +18,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_24_state_reducers.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_18_state_reducers.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_24_state_reducers.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_18_state_reducers.py ``` ## Source ```python ---8<-- "examples/notebook_24_state_reducers.py" +--8<-- "examples/notebook_18_state_reducers.py" ``` diff --git a/docs/notebooks/notebook_25_human_in_the_loop.md b/docs/notebooks/notebook_19_human_in_the_loop.md similarity index 89% rename from docs/notebooks/notebook_25_human_in_the_loop.md rename to docs/notebooks/notebook_19_human_in_the_loop.md index 3bd6c8fe..933ad890 100644 --- a/docs/notebooks/notebook_25_human_in_the_loop.md +++ b/docs/notebooks/notebook_19_human_in_the_loop.md @@ -22,11 +22,11 @@ This notebook doesn't call any LLM, so the model provider doesn't matter: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_25_human_in_the_loop.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_19_human_in_the_loop.py ``` ## Source ```python ---8<-- "examples/notebook_25_human_in_the_loop.py" +--8<-- "examples/notebook_19_human_in_the_loop.py" ``` diff --git a/docs/notebooks/notebook_26_advanced_patterns.md b/docs/notebooks/notebook_20_advanced_patterns.md similarity index 84% rename from docs/notebooks/notebook_26_advanced_patterns.md rename to docs/notebooks/notebook_20_advanced_patterns.md index b3a475ae..cd56a913 100644 --- a/docs/notebooks/notebook_26_advanced_patterns.md +++ b/docs/notebooks/notebook_20_advanced_patterns.md @@ -21,13 +21,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_26_advanced_patterns.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_20_advanced_patterns.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_26_advanced_patterns.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_20_advanced_patterns.py ``` ## Source ```python ---8<-- "examples/notebook_26_advanced_patterns.py" +--8<-- "examples/notebook_20_advanced_patterns.py" ``` diff --git a/docs/notebooks/notebook_27_composition.md b/docs/notebooks/notebook_21_composition.md similarity index 81% rename from docs/notebooks/notebook_27_composition.md rename to docs/notebooks/notebook_21_composition.md index fd256acc..2636f622 100644 --- a/docs/notebooks/notebook_27_composition.md +++ b/docs/notebooks/notebook_21_composition.md @@ -16,13 +16,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_27_composition.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_21_composition.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_27_composition.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_21_composition.py ``` ## Source ```python ---8<-- "examples/notebook_27_composition.py" +--8<-- "examples/notebook_21_composition.py" ``` diff --git a/docs/notebooks/notebook_28_graph_advanced.md b/docs/notebooks/notebook_22_graph_advanced.md similarity index 82% rename from docs/notebooks/notebook_28_graph_advanced.md rename to docs/notebooks/notebook_22_graph_advanced.md index 5e082bf5..8fdad4e0 100644 --- a/docs/notebooks/notebook_28_graph_advanced.md +++ b/docs/notebooks/notebook_22_graph_advanced.md @@ -18,13 +18,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_28_graph_advanced.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_22_graph_advanced.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_28_graph_advanced.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_22_graph_advanced.py ``` ## Source ```python ---8<-- "examples/notebook_28_graph_advanced.py" +--8<-- "examples/notebook_22_graph_advanced.py" ``` diff --git a/docs/notebooks/notebook_29_functional_api.md b/docs/notebooks/notebook_23_functional_api.md similarity index 82% rename from docs/notebooks/notebook_29_functional_api.md rename to docs/notebooks/notebook_23_functional_api.md index 51b3ff06..0452e7e5 100644 --- a/docs/notebooks/notebook_29_functional_api.md +++ b/docs/notebooks/notebook_23_functional_api.md @@ -18,13 +18,13 @@ What you'll see: Runs on the same OCI GenAI default as the rest of the notebooks: ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_29_functional_api.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_23_functional_api.py # or, fully offline: -LOCUS_MODEL_PROVIDER=mock python examples/notebook_29_functional_api.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_23_functional_api.py ``` ## Source ```python ---8<-- "examples/notebook_29_functional_api.py" +--8<-- "examples/notebook_23_functional_api.py" ``` diff --git a/docs/notebooks/notebook_30_swarm_multiagent.md b/docs/notebooks/notebook_24_swarm_multiagent.md similarity index 92% rename from docs/notebooks/notebook_30_swarm_multiagent.md rename to docs/notebooks/notebook_24_swarm_multiagent.md index 901025d5..9ceef75d 100644 --- a/docs/notebooks/notebook_30_swarm_multiagent.md +++ b/docs/notebooks/notebook_24_swarm_multiagent.md @@ -26,7 +26,7 @@ This notebook covers: ## Run ```bash -python examples/notebook_30_swarm_multiagent.py +python examples/notebook_24_swarm_multiagent.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -37,5 +37,5 @@ present the swarm talks to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_30_swarm_multiagent.py" +--8<-- "examples/notebook_24_swarm_multiagent.py" ``` diff --git a/docs/notebooks/notebook_31_agent_handoff.md b/docs/notebooks/notebook_25_agent_handoff.md similarity index 93% rename from docs/notebooks/notebook_31_agent_handoff.md rename to docs/notebooks/notebook_25_agent_handoff.md index 51e73849..b5a174ba 100644 --- a/docs/notebooks/notebook_31_agent_handoff.md +++ b/docs/notebooks/notebook_25_agent_handoff.md @@ -27,7 +27,7 @@ This notebook covers: ## Run ```bash -python examples/notebook_31_agent_handoff.py +python examples/notebook_25_agent_handoff.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -38,5 +38,5 @@ present the agents talk to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_31_agent_handoff.py" +--8<-- "examples/notebook_25_agent_handoff.py" ``` diff --git a/docs/notebooks/notebook_32_orchestrator_pattern.md b/docs/notebooks/notebook_26_orchestrator_pattern.md similarity index 92% rename from docs/notebooks/notebook_32_orchestrator_pattern.md rename to docs/notebooks/notebook_26_orchestrator_pattern.md index 0c0b05be..7db6882d 100644 --- a/docs/notebooks/notebook_32_orchestrator_pattern.md +++ b/docs/notebooks/notebook_26_orchestrator_pattern.md @@ -26,7 +26,7 @@ This notebook covers: ## Run ```bash -python examples/notebook_32_orchestrator_pattern.py +python examples/notebook_26_orchestrator_pattern.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -37,5 +37,5 @@ present the agents talk to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_32_orchestrator_pattern.py" +--8<-- "examples/notebook_26_orchestrator_pattern.py" ``` diff --git a/docs/notebooks/notebook_33_specialist_agents.md b/docs/notebooks/notebook_27_specialist_agents.md similarity index 92% rename from docs/notebooks/notebook_33_specialist_agents.md rename to docs/notebooks/notebook_27_specialist_agents.md index e97f19c9..d974593b 100644 --- a/docs/notebooks/notebook_33_specialist_agents.md +++ b/docs/notebooks/notebook_27_specialist_agents.md @@ -26,7 +26,7 @@ This notebook covers: ## Run ```bash -python examples/notebook_33_specialist_agents.py +python examples/notebook_27_specialist_agents.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -37,5 +37,5 @@ present the specialists talk to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_33_specialist_agents.py" +--8<-- "examples/notebook_27_specialist_agents.py" ``` diff --git a/docs/notebooks/notebook_34_a2a_protocol.md b/docs/notebooks/notebook_28_a2a_protocol.md similarity index 94% rename from docs/notebooks/notebook_34_a2a_protocol.md rename to docs/notebooks/notebook_28_a2a_protocol.md index b0cbba19..d2224b73 100644 --- a/docs/notebooks/notebook_34_a2a_protocol.md +++ b/docs/notebooks/notebook_28_a2a_protocol.md @@ -27,7 +27,7 @@ This notebook covers: ## Run ```bash -python examples/notebook_34_a2a_protocol.py +python examples/notebook_28_a2a_protocol.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -42,5 +42,5 @@ against it; expect a few seconds of warm-up before the first ## Source ```python ---8<-- "examples/notebook_34_a2a_protocol.py" +--8<-- "examples/notebook_28_a2a_protocol.py" ``` diff --git a/docs/notebooks/notebook_35_deepagent.md b/docs/notebooks/notebook_29_deepagent.md similarity index 96% rename from docs/notebooks/notebook_35_deepagent.md rename to docs/notebooks/notebook_29_deepagent.md index 356eea8e..9592c804 100644 --- a/docs/notebooks/notebook_35_deepagent.md +++ b/docs/notebooks/notebook_29_deepagent.md @@ -41,7 +41,7 @@ sentence — `(ToolCalled("submit") & ConfidenceMet(0.85)) ## Run ```bash -python examples/notebook_35_deepagent.py +python examples/notebook_29_deepagent.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -56,5 +56,5 @@ and OpenSearch live in ## Source ```python ---8<-- "examples/notebook_35_deepagent.py" +--8<-- "examples/notebook_29_deepagent.py" ``` diff --git a/docs/notebooks/notebook_36_map_reduce_code_review.md b/docs/notebooks/notebook_30_map_reduce_code_review.md similarity index 92% rename from docs/notebooks/notebook_36_map_reduce_code_review.md rename to docs/notebooks/notebook_30_map_reduce_code_review.md index d0c379ad..c82f1954 100644 --- a/docs/notebooks/notebook_36_map_reduce_code_review.md +++ b/docs/notebooks/notebook_30_map_reduce_code_review.md @@ -28,7 +28,7 @@ Diff splitter ──> N reviewers (parallel via Send) ──> Synthesizer ## Run ```bash -python examples/notebook_36_map_reduce_code_review.py +python examples/notebook_30_map_reduce_code_review.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -39,5 +39,5 @@ present the reviewers talk to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_36_map_reduce_code_review.py" +--8<-- "examples/notebook_30_map_reduce_code_review.py" ``` diff --git a/docs/notebooks/notebook_37_supervisor_critic_loop.md b/docs/notebooks/notebook_31_supervisor_critic_loop.md similarity index 91% rename from docs/notebooks/notebook_37_supervisor_critic_loop.md rename to docs/notebooks/notebook_31_supervisor_critic_loop.md index e4eae6de..9677610a 100644 --- a/docs/notebooks/notebook_37_supervisor_critic_loop.md +++ b/docs/notebooks/notebook_31_supervisor_critic_loop.md @@ -30,7 +30,7 @@ START → research → write → critique → END (approve) ## Run ```bash -python examples/notebook_37_supervisor_critic_loop.py +python examples/notebook_31_supervisor_critic_loop.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -41,5 +41,5 @@ present the roles talk to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_37_supervisor_critic_loop.py" +--8<-- "examples/notebook_31_supervisor_critic_loop.py" ``` diff --git a/docs/notebooks/notebook_38_debate_with_judge.md b/docs/notebooks/notebook_32_debate_with_judge.md similarity index 92% rename from docs/notebooks/notebook_38_debate_with_judge.md rename to docs/notebooks/notebook_32_debate_with_judge.md index 36d58733..52618da3 100644 --- a/docs/notebooks/notebook_38_debate_with_judge.md +++ b/docs/notebooks/notebook_32_debate_with_judge.md @@ -29,7 +29,7 @@ PRO r0 → CON r0 → PRO r1 → CON r1 → ... → judge → END ## Run ```bash -python examples/notebook_38_debate_with_judge.py +python examples/notebook_32_debate_with_judge.py ``` The default provider is OCI Generative AI. Pick a model that supports @@ -40,5 +40,5 @@ cleanly with setup instructions. ## Source ```python ---8<-- "examples/notebook_38_debate_with_judge.py" +--8<-- "examples/notebook_32_debate_with_judge.py" ``` diff --git a/docs/notebooks/notebook_39_multiagent_human_in_loop.md b/docs/notebooks/notebook_33_multiagent_human_in_loop.md similarity index 93% rename from docs/notebooks/notebook_39_multiagent_human_in_loop.md rename to docs/notebooks/notebook_33_multiagent_human_in_loop.md index 3feb1360..74df1728 100644 --- a/docs/notebooks/notebook_39_multiagent_human_in_loop.md +++ b/docs/notebooks/notebook_33_multiagent_human_in_loop.md @@ -33,7 +33,7 @@ gate for irreversible actions. This notebook walks three combinations. ## Run ```bash -python examples/notebook_39_multiagent_human_in_loop.py +python examples/notebook_33_multiagent_human_in_loop.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -44,5 +44,5 @@ present the specialists talk to a live OCI model; canonical picks are ## Source ```python ---8<-- "examples/notebook_39_multiagent_human_in_loop.py" +--8<-- "examples/notebook_33_multiagent_human_in_loop.py" ``` diff --git a/docs/notebooks/notebook_40_emergent_routing.md b/docs/notebooks/notebook_34_emergent_routing.md similarity index 93% rename from docs/notebooks/notebook_40_emergent_routing.md rename to docs/notebooks/notebook_34_emergent_routing.md index 8933041a..00eb1305 100644 --- a/docs/notebooks/notebook_40_emergent_routing.md +++ b/docs/notebooks/notebook_34_emergent_routing.md @@ -39,7 +39,7 @@ The picker is strictly limited to disambiguation. The compiler still: ## Run ```bash -python examples/notebook_40_emergent_routing.py +python examples/notebook_34_emergent_routing.py ``` The default provider is OCI Generative AI. With `~/.oci/config` @@ -56,11 +56,11 @@ rationale (on the SSE event) explains why. - [Notebook 53 — cognitive router (default rule-based path)](notebook_58_cognitive_router.md) - [Concepts: cognitive router](../concepts/router.md) — the filter-then-pick invariant plus observability schema. -- [Notebook 27 — orchestrator pattern](notebook_32_orchestrator_pattern.md) +- [Notebook 27 — orchestrator pattern](notebook_26_orchestrator_pattern.md) — for emergent coordination *inside* a protocol. ## Source ```python ---8<-- "examples/notebook_40_emergent_routing.py" +--8<-- "examples/notebook_34_emergent_routing.py" ``` diff --git a/docs/notebooks/notebook_41_structured_output.md b/docs/notebooks/notebook_35_structured_output.md similarity index 86% rename from docs/notebooks/notebook_41_structured_output.md rename to docs/notebooks/notebook_35_structured_output.md index b9bef3be..4d896471 100644 --- a/docs/notebooks/notebook_41_structured_output.md +++ b/docs/notebooks/notebook_35_structured_output.md @@ -19,13 +19,13 @@ a `[model call: X.XXs · prompt→completion tokens]` banner. OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_41_structured_output.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_35_structured_output.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_41_structured_output.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_35_structured_output.py ``` ## Prerequisites @@ -39,5 +39,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_41_structured_output.py ## Source ```python ---8<-- "examples/notebook_41_structured_output.py" +--8<-- "examples/notebook_35_structured_output.py" ``` diff --git a/docs/notebooks/notebook_42_reasoning_patterns.md b/docs/notebooks/notebook_36_reasoning_patterns.md similarity index 86% rename from docs/notebooks/notebook_42_reasoning_patterns.md rename to docs/notebooks/notebook_36_reasoning_patterns.md index f8aae675..cfcb93f3 100644 --- a/docs/notebooks/notebook_42_reasoning_patterns.md +++ b/docs/notebooks/notebook_36_reasoning_patterns.md @@ -19,13 +19,13 @@ tokens]` so you can see the round-trip. OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_42_reasoning_patterns.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_36_reasoning_patterns.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_42_reasoning_patterns.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_36_reasoning_patterns.py ``` ## Prerequisites @@ -39,5 +39,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_42_reasoning_patterns.py ## Source ```python ---8<-- "examples/notebook_42_reasoning_patterns.py" +--8<-- "examples/notebook_36_reasoning_patterns.py" ``` diff --git a/docs/notebooks/notebook_43_gsar_typed_grounding.md b/docs/notebooks/notebook_37_gsar_typed_grounding.md similarity index 70% rename from docs/notebooks/notebook_43_gsar_typed_grounding.md rename to docs/notebooks/notebook_37_gsar_typed_grounding.md index 10cdf044..6a2a097e 100644 --- a/docs/notebooks/notebook_43_gsar_typed_grounding.md +++ b/docs/notebooks/notebook_37_gsar_typed_grounding.md @@ -1,9 +1,10 @@ # GSAR Typed Grounding GSAR (Grounded Structured Answer Reasoning) is the Locus layer from -`arXiv:2604.23366` (Kamelhar 2026). It partitions an answer's claims -into four buckets, scores them against evidence, and decides whether -to proceed, regenerate, or replan. +[Federico A. Kamelhar (2026), arXiv:2604.23366](https://arxiv.org/abs/2604.23366). +It partitions an answer's claims into four buckets, scores them +against evidence, and decides whether to proceed, regenerate, or +replan. - The four-way partition (grounded / ungrounded / contradicted / complementary) as a Pydantic type. @@ -19,13 +20,13 @@ to proceed, regenerate, or replan. OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_43_gsar_typed_grounding.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_37_gsar_typed_grounding.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_43_gsar_typed_grounding.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_37_gsar_typed_grounding.py ``` ## Prerequisites @@ -38,5 +39,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_43_gsar_typed_grounding.py ## Source ```python ---8<-- "examples/notebook_43_gsar_typed_grounding.py" +--8<-- "examples/notebook_37_gsar_typed_grounding.py" ``` diff --git a/docs/notebooks/notebook_44_rag_basics.md b/docs/notebooks/notebook_38_rag_basics.md similarity index 89% rename from docs/notebooks/notebook_44_rag_basics.md rename to docs/notebooks/notebook_38_rag_basics.md index ebe04369..3ca49240 100644 --- a/docs/notebooks/notebook_44_rag_basics.md +++ b/docs/notebooks/notebook_38_rag_basics.md @@ -19,13 +19,13 @@ OCI GenAI is the default for embeddings (auto-detected from `~/.oci/config`): ```bash -python examples/notebook_44_rag_basics.py +python examples/notebook_38_rag_basics.py ``` Offline (skips the live demo cleanly when env vars are missing): ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_44_rag_basics.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_38_rag_basics.py ``` ## Prerequisites @@ -47,5 +47,5 @@ export OCI_COMPARTMENT=ocid1.compartment.oc1..… ## Source ```python ---8<-- "examples/notebook_44_rag_basics.py" +--8<-- "examples/notebook_38_rag_basics.py" ``` diff --git a/docs/notebooks/notebook_45_rag_providers.md b/docs/notebooks/notebook_39_rag_providers.md similarity index 90% rename from docs/notebooks/notebook_45_rag_providers.md rename to docs/notebooks/notebook_39_rag_providers.md index d8b24203..35559348 100644 --- a/docs/notebooks/notebook_45_rag_providers.md +++ b/docs/notebooks/notebook_39_rag_providers.md @@ -25,13 +25,13 @@ OCI GenAI is the default for embeddings (auto-detected from `~/.oci/config`): ```bash -python examples/notebook_45_rag_providers.py +python examples/notebook_39_rag_providers.py ``` Offline (skips the live demo cleanly when env vars are missing): ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_45_rag_providers.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_39_rag_providers.py ``` ## Prerequisites @@ -50,5 +50,5 @@ export OCI_COMPARTMENT=ocid1.compartment.oc1..… ## Source ```python ---8<-- "examples/notebook_45_rag_providers.py" +--8<-- "examples/notebook_39_rag_providers.py" ``` diff --git a/docs/notebooks/notebook_46_rag_agents.md b/docs/notebooks/notebook_40_rag_agents.md similarity index 88% rename from docs/notebooks/notebook_46_rag_agents.md rename to docs/notebooks/notebook_40_rag_agents.md index d8af7836..99ceaf7b 100644 --- a/docs/notebooks/notebook_46_rag_agents.md +++ b/docs/notebooks/notebook_40_rag_agents.md @@ -23,13 +23,13 @@ pgvector, in-memory). OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_46_rag_agents.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_40_rag_agents.py ``` Offline (skips the live demo cleanly when env vars are missing): ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_46_rag_agents.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_40_rag_agents.py ``` ## Prerequisites @@ -46,5 +46,5 @@ export OCI_COMPARTMENT=ocid1.compartment.oc1..… ## Source ```python ---8<-- "examples/notebook_46_rag_agents.py" +--8<-- "examples/notebook_40_rag_agents.py" ``` diff --git a/docs/notebooks/notebook_06_oracle_26ai_rag.md b/docs/notebooks/notebook_41_oracle_26ai_rag.md similarity index 97% rename from docs/notebooks/notebook_06_oracle_26ai_rag.md rename to docs/notebooks/notebook_41_oracle_26ai_rag.md index 942d7065..e9e3cfbb 100644 --- a/docs/notebooks/notebook_06_oracle_26ai_rag.md +++ b/docs/notebooks/notebook_41_oracle_26ai_rag.md @@ -46,7 +46,7 @@ no half-initialised state. ## Run ```bash -python examples/notebook_06_oracle_26ai_rag.py +python examples/notebook_41_oracle_26ai_rag.py ``` ## Schema hygiene @@ -71,5 +71,5 @@ for the `CREATE USER locus_app` / `CREATE VECTOR INDEX` DDL. ## Source ```python ---8<-- "examples/notebook_06_oracle_26ai_rag.py" +--8<-- "examples/notebook_41_oracle_26ai_rag.py" ``` diff --git a/docs/notebooks/notebook_08_oracle_adb_loader.md b/docs/notebooks/notebook_42_oracle_adb_loader.md similarity index 91% rename from docs/notebooks/notebook_08_oracle_adb_loader.md rename to docs/notebooks/notebook_42_oracle_adb_loader.md index be8e4c69..c41406cd 100644 --- a/docs/notebooks/notebook_08_oracle_adb_loader.md +++ b/docs/notebooks/notebook_42_oracle_adb_loader.md @@ -59,14 +59,14 @@ no half-initialised state. ## Run ```bash -python examples/notebook_08_oracle_adb_loader.py +python examples/notebook_42_oracle_adb_loader.py ``` ## See also -- [Notebook 06 — Oracle 26ai RAG](notebook_06_oracle_26ai_rag.md) -- [Notebook 09 — Oracle in-DB chunker](notebook_09_oracle_indb_chunker.md) -- [Notebook 10 — Oracle in-DB embeddings](notebook_10_oracle_indb_embeddings.md) +- [Notebook 06 — Oracle 26ai RAG](notebook_41_oracle_26ai_rag.md) +- [Notebook 09 — Oracle in-DB chunker](notebook_43_oracle_indb_chunker.md) +- [Notebook 10 — Oracle in-DB embeddings](notebook_44_oracle_indb_embeddings.md) - [Oracle Autonomous Database — documentation](https://docs.oracle.com/en/cloud/paas/autonomous-database/index.html) - [Oracle AI Database 26ai — AI Vector Search User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/index.html) - [python-oracledb driver](https://python-oracledb.readthedocs.io/en/latest/index.html) @@ -74,5 +74,5 @@ python examples/notebook_08_oracle_adb_loader.py ## Source ```python ---8<-- "examples/notebook_08_oracle_adb_loader.py" +--8<-- "examples/notebook_42_oracle_adb_loader.py" ``` diff --git a/docs/notebooks/notebook_09_oracle_indb_chunker.md b/docs/notebooks/notebook_43_oracle_indb_chunker.md similarity index 90% rename from docs/notebooks/notebook_09_oracle_indb_chunker.md rename to docs/notebooks/notebook_43_oracle_indb_chunker.md index 9648304e..0b036dab 100644 --- a/docs/notebooks/notebook_09_oracle_indb_chunker.md +++ b/docs/notebooks/notebook_43_oracle_indb_chunker.md @@ -58,14 +58,14 @@ no half-initialised state. ## Run ```bash -python examples/notebook_09_oracle_indb_chunker.py +python examples/notebook_43_oracle_indb_chunker.py ``` ## See also -- [Notebook 06 — Oracle 26ai RAG](notebook_06_oracle_26ai_rag.md) -- [Notebook 08 — Oracle ADB document loader](notebook_08_oracle_adb_loader.md) -- [Notebook 10 — Oracle in-DB embeddings](notebook_10_oracle_indb_embeddings.md) +- [Notebook 06 — Oracle 26ai RAG](notebook_41_oracle_26ai_rag.md) +- [Notebook 08 — Oracle ADB document loader](notebook_42_oracle_adb_loader.md) +- [Notebook 10 — Oracle in-DB embeddings](notebook_44_oracle_indb_embeddings.md) - [Oracle AI Database 26ai — AI Vector Search User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/index.html) - [`DBMS_VECTOR_CHAIN` PL/SQL package reference](https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/dbms_vector_chain1.html) - [python-oracledb driver](https://python-oracledb.readthedocs.io/en/latest/index.html) @@ -73,5 +73,5 @@ python examples/notebook_09_oracle_indb_chunker.py ## Source ```python ---8<-- "examples/notebook_09_oracle_indb_chunker.py" +--8<-- "examples/notebook_43_oracle_indb_chunker.py" ``` diff --git a/docs/notebooks/notebook_10_oracle_indb_embeddings.md b/docs/notebooks/notebook_44_oracle_indb_embeddings.md similarity index 92% rename from docs/notebooks/notebook_10_oracle_indb_embeddings.md rename to docs/notebooks/notebook_44_oracle_indb_embeddings.md index c92df7e1..7d0684c1 100644 --- a/docs/notebooks/notebook_10_oracle_indb_embeddings.md +++ b/docs/notebooks/notebook_44_oracle_indb_embeddings.md @@ -77,14 +77,14 @@ case. ## Run ```bash -python examples/notebook_10_oracle_indb_embeddings.py +python examples/notebook_44_oracle_indb_embeddings.py ``` ## See also -- [Notebook 06 — Oracle 26ai RAG](notebook_06_oracle_26ai_rag.md) -- [Notebook 08 — Oracle ADB document loader](notebook_08_oracle_adb_loader.md) -- [Notebook 09 — Oracle in-DB chunker](notebook_09_oracle_indb_chunker.md) +- [Notebook 06 — Oracle 26ai RAG](notebook_41_oracle_26ai_rag.md) +- [Notebook 08 — Oracle ADB document loader](notebook_42_oracle_adb_loader.md) +- [Notebook 09 — Oracle in-DB chunker](notebook_43_oracle_indb_chunker.md) - [Oracle AI Database 26ai — AI Vector Search User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/index.html) - [`DBMS_VECTOR_CHAIN` PL/SQL package reference](https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/dbms_vector_chain1.html) - [python-oracledb driver](https://python-oracledb.readthedocs.io/en/latest/index.html) @@ -92,5 +92,5 @@ python examples/notebook_10_oracle_indb_embeddings.py ## Source ```python ---8<-- "examples/notebook_10_oracle_indb_embeddings.py" +--8<-- "examples/notebook_44_oracle_indb_embeddings.py" ``` diff --git a/docs/notebooks/notebook_47_mcp_integration.md b/docs/notebooks/notebook_45_mcp_integration.md similarity index 85% rename from docs/notebooks/notebook_47_mcp_integration.md rename to docs/notebooks/notebook_45_mcp_integration.md index 57c573d6..e6f1f0c6 100644 --- a/docs/notebooks/notebook_47_mcp_integration.md +++ b/docs/notebooks/notebook_45_mcp_integration.md @@ -20,13 +20,13 @@ OCI GenAI drives the agent by default. The MCP layer is transport-only OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_47_mcp_integration.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_45_mcp_integration.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_47_mcp_integration.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_45_mcp_integration.py ``` ## Prerequisites @@ -40,5 +40,5 @@ See for the MCP specification. ## Source ```python ---8<-- "examples/notebook_47_mcp_integration.py" +--8<-- "examples/notebook_45_mcp_integration.py" ``` diff --git a/docs/notebooks/notebook_48_playbooks.md b/docs/notebooks/notebook_46_playbooks.md similarity index 87% rename from docs/notebooks/notebook_48_playbooks.md rename to docs/notebooks/notebook_46_playbooks.md index 8e069105..e6c83b26 100644 --- a/docs/notebooks/notebook_48_playbooks.md +++ b/docs/notebooks/notebook_46_playbooks.md @@ -25,13 +25,13 @@ to the structured execution mechanics — every section prints OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_48_playbooks.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_46_playbooks.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_48_playbooks.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_46_playbooks.py ``` ## Prerequisites @@ -42,5 +42,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_48_playbooks.py ## Source ```python ---8<-- "examples/notebook_48_playbooks.py" +--8<-- "examples/notebook_46_playbooks.py" ``` diff --git a/docs/notebooks/notebook_49_plugins.md b/docs/notebooks/notebook_47_plugins.md similarity index 84% rename from docs/notebooks/notebook_49_plugins.md rename to docs/notebooks/notebook_47_plugins.md index 7e34bd66..01ebc5c4 100644 --- a/docs/notebooks/notebook_49_plugins.md +++ b/docs/notebooks/notebook_47_plugins.md @@ -18,13 +18,13 @@ automatically. OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_49_plugins.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_47_plugins.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_49_plugins.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_47_plugins.py ``` ## Prerequisites @@ -35,5 +35,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_49_plugins.py ## Source ```python ---8<-- "examples/notebook_49_plugins.py" +--8<-- "examples/notebook_47_plugins.py" ``` diff --git a/docs/notebooks/notebook_50_skills.md b/docs/notebooks/notebook_48_skills.md similarity index 87% rename from docs/notebooks/notebook_50_skills.md rename to docs/notebooks/notebook_48_skills.md index 6cf167db..aa49dc2f 100644 --- a/docs/notebooks/notebook_50_skills.md +++ b/docs/notebooks/notebook_48_skills.md @@ -20,13 +20,13 @@ system prompt small and the agent focused. OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_50_skills.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_48_skills.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_50_skills.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_48_skills.py ``` ## Prerequisites @@ -39,5 +39,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_50_skills.py ## Source ```python ---8<-- "examples/notebook_50_skills.py" +--8<-- "examples/notebook_48_skills.py" ``` diff --git a/docs/notebooks/notebook_51_steering.md b/docs/notebooks/notebook_49_steering.md similarity index 85% rename from docs/notebooks/notebook_51_steering.md rename to docs/notebooks/notebook_49_steering.md index 44bac55b..befa843d 100644 --- a/docs/notebooks/notebook_51_steering.md +++ b/docs/notebooks/notebook_49_steering.md @@ -22,13 +22,13 @@ OCI GenAI drives both the agent and the steering model by default. OCI GenAI is the default (auto-detected from `~/.oci/config`): ```bash -LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_51_steering.py +LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_49_steering.py ``` Offline: ```bash -LOCUS_MODEL_PROVIDER=mock python examples/notebook_51_steering.py +LOCUS_MODEL_PROVIDER=mock python examples/notebook_49_steering.py ``` ## Prerequisites @@ -39,5 +39,5 @@ LOCUS_MODEL_PROVIDER=mock python examples/notebook_51_steering.py ## Source ```python ---8<-- "examples/notebook_51_steering.py" +--8<-- "examples/notebook_49_steering.py" ``` diff --git a/docs/notebooks/notebook_52_guardrails_security.md b/docs/notebooks/notebook_50_guardrails_security.md similarity index 76% rename from docs/notebooks/notebook_52_guardrails_security.md rename to docs/notebooks/notebook_50_guardrails_security.md index f8ce35e6..eb7b6f8f 100644 --- a/docs/notebooks/notebook_52_guardrails_security.md +++ b/docs/notebooks/notebook_50_guardrails_security.md @@ -13,18 +13,18 @@ so the safety policy is exercised live, not described in the abstract. Run it (OCI Generative AI is the default; auto-detected from `~/.oci/config`): - python examples/notebook_52_guardrails_security.py + python examples/notebook_50_guardrails_security.py Offline / no credentials: - LOCUS_MODEL_PROVIDER=mock python examples/notebook_52_guardrails_security.py + LOCUS_MODEL_PROVIDER=mock python examples/notebook_50_guardrails_security.py Pin a specific OCI model: - LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_52_guardrails_security.py + LOCUS_MODEL_ID=openai.gpt-4.1 python examples/notebook_50_guardrails_security.py ## Source ```python ---8<-- "examples/notebook_52_guardrails_security.py" +--8<-- "examples/notebook_50_guardrails_security.py" ``` diff --git a/docs/notebooks/notebook_53_guardrails_advanced.md b/docs/notebooks/notebook_51_guardrails_advanced.md similarity index 76% rename from docs/notebooks/notebook_53_guardrails_advanced.md rename to docs/notebooks/notebook_51_guardrails_advanced.md index 1481ad8f..cd1891b9 100644 --- a/docs/notebooks/notebook_53_guardrails_advanced.md +++ b/docs/notebooks/notebook_51_guardrails_advanced.md @@ -11,14 +11,14 @@ characters appear in the prompt. Run it (OCI Generative AI is the default; auto-detected from `~/.oci/config`): - python examples/notebook_53_guardrails_advanced.py + python examples/notebook_51_guardrails_advanced.py Offline: - LOCUS_MODEL_PROVIDER=mock python examples/notebook_53_guardrails_advanced.py + LOCUS_MODEL_PROVIDER=mock python examples/notebook_51_guardrails_advanced.py ## Source ```python ---8<-- "examples/notebook_53_guardrails_advanced.py" +--8<-- "examples/notebook_51_guardrails_advanced.py" ``` diff --git a/docs/notebooks/notebook_54_checkpoint_backends.md b/docs/notebooks/notebook_52_checkpoint_backends.md similarity index 83% rename from docs/notebooks/notebook_54_checkpoint_backends.md rename to docs/notebooks/notebook_52_checkpoint_backends.md index 32b72192..84a134e1 100644 --- a/docs/notebooks/notebook_54_checkpoint_backends.md +++ b/docs/notebooks/notebook_52_checkpoint_backends.md @@ -20,16 +20,16 @@ Run it (requires a running Autonomous Database with its wallet on disk): export ORACLE_PASSWORD='' export ORACLE_WALLET=~/.oci/wallets/mydb export ORACLE_WALLET_PASSWORD='' # if encrypted - python examples/notebook_54_checkpoint_backends.py + python examples/notebook_52_checkpoint_backends.py Without the env vars the notebook prints what's missing and exits cleanly so CI stays green. The in-memory checkpointer covered in -[notebook 10](notebook_16_agent_memory.md) is the developer default; -the [Oracle Database 26ai checkpointer](notebook_07_oracle_26ai_checkpointer.md) +[notebook 10](notebook_08_agent_memory.md) is the developer default; +the [Oracle Database 26ai checkpointer](notebook_53_oracle_26ai_checkpointer.md) covered in notebook 06 is the production default. ## Source ```python ---8<-- "examples/notebook_54_checkpoint_backends.py" +--8<-- "examples/notebook_52_checkpoint_backends.py" ``` diff --git a/docs/notebooks/notebook_07_oracle_26ai_checkpointer.md b/docs/notebooks/notebook_53_oracle_26ai_checkpointer.md similarity index 94% rename from docs/notebooks/notebook_07_oracle_26ai_checkpointer.md rename to docs/notebooks/notebook_53_oracle_26ai_checkpointer.md index 130106cf..009fab05 100644 --- a/docs/notebooks/notebook_07_oracle_26ai_checkpointer.md +++ b/docs/notebooks/notebook_53_oracle_26ai_checkpointer.md @@ -40,7 +40,7 @@ state. ## Run ```bash -python examples/notebook_07_oracle_26ai_checkpointer.py +python examples/notebook_53_oracle_26ai_checkpointer.py ``` ## Schema hygiene @@ -55,7 +55,7 @@ and the rotation guidance. ## See also - [Concepts — Checkpointers & Store](../concepts/checkpointers.md) -- [Notebook 05 — Oracle 26ai RAG (shares the same wallet)](notebook_06_oracle_26ai_rag.md) +- [Notebook 05 — Oracle 26ai RAG (shares the same wallet)](notebook_41_oracle_26ai_rag.md) - [Oracle Database 26ai — documentation](https://docs.oracle.com/en/database/oracle/oracle-database/23/index.html) - [Oracle Autonomous Database — documentation](https://docs.oracle.com/en/cloud/paas/autonomous-database/index.html) - [python-oracledb driver](https://python-oracledb.readthedocs.io/en/latest/index.html) @@ -63,5 +63,5 @@ and the rotation guidance. ## Source ```python ---8<-- "examples/notebook_07_oracle_26ai_checkpointer.py" +--8<-- "examples/notebook_53_oracle_26ai_checkpointer.py" ``` diff --git a/docs/notebooks/notebook_12_oracle_versioned_saver.md b/docs/notebooks/notebook_54_oracle_versioned_saver.md similarity index 93% rename from docs/notebooks/notebook_12_oracle_versioned_saver.md rename to docs/notebooks/notebook_54_oracle_versioned_saver.md index 56eb531b..046a3247 100644 --- a/docs/notebooks/notebook_12_oracle_versioned_saver.md +++ b/docs/notebooks/notebook_54_oracle_versioned_saver.md @@ -46,12 +46,12 @@ exits cleanly — no traceback. ## Run ```bash -python examples/notebook_12_oracle_versioned_saver.py +python examples/notebook_54_oracle_versioned_saver.py ``` ## See also -- [Oracle 26ai checkpointer (single-row)](notebook_07_oracle_26ai_checkpointer.md) +- [Oracle 26ai checkpointer (single-row)](notebook_53_oracle_26ai_checkpointer.md) - [Concepts → Checkpointers & Store](../concepts/checkpointers.md) - [Oracle Database 26ai — documentation](https://docs.oracle.com/en/database/oracle/oracle-database/23/index.html) - [Oracle Autonomous Database — documentation](https://docs.oracle.com/en/cloud/paas/autonomous-database/index.html) @@ -60,5 +60,5 @@ python examples/notebook_12_oracle_versioned_saver.py ## Source ```python ---8<-- "examples/notebook_12_oracle_versioned_saver.py" +--8<-- "examples/notebook_54_oracle_versioned_saver.py" ``` diff --git a/docs/notebooks/notebook_70_oci_tools.md b/docs/notebooks/notebook_70_oci_tools.md new file mode 100644 index 00000000..bb3e5755 --- /dev/null +++ b/docs/notebooks/notebook_70_oci_tools.md @@ -0,0 +1,67 @@ +# Notebook 70: Drive the entire OCI control plane from natural language — `use_oci` + `describe_oci` + +Locus ships two built-in tools that, together, expose every OCI Python +SDK operation to an agent without per-service plumbing: + +- ``describe_oci(service?, client?, operation?)`` — runtime + introspection. Progressively zooms into the SDK: list services + (~190), list clients in a service, list operations on a client, + return the parameter schema for one operation (parsed from the + OCI SDK docstring). +- ``use_oci(service, client, operation, parameters, ...)`` — execute + any introspected call. Builds the right ``oci..`` + with the requested auth, invokes the method, and serialises the + response (snake_case Python attrs are rendered with their wire + camelCase names via ``attribute_map``). + +The agent's loop is: hand the model the two tools, the model picks +``service`` + ``client`` + ``operation`` from natural-language intent, +optionally calls ``describe_oci`` to verify the parameter shape, then +calls ``use_oci`` to run it. One open-spec primitive covers the +entire OCI surface — Identity, Compute, Database, Object Storage, +Networking, Vault, Functions, Bastion, GenAI, the lot. + +Safety: ``use_oci`` is read-only by default. Operations whose names +don't start with a read-only prefix (``list_``, ``get_``, ``head_``, +``summarize_``, ``describe_``, ``search_``, ``fetch_``, ``compute_``, +``preview_``, ``validate_``, ``test_``) are refused unless either +``allow_mutations=True`` is passed explicitly or the env var +``LOCUS_USE_OCI_ALLOW_MUTATIONS=1`` is set. + +Key concepts: + +- A *service* is an OCI Python SDK submodule under ``oci.*`` + (snake_case: ``identity``, ``core``, ``database``, ``object_storage``). +- A *client* is a class inside the service module (CamelCase, ends in + ``Client``: ``IdentityClient``, ``ComputeClient``, ``DatabaseClient``). +- An *operation* is a method on the client (snake_case, mirrors the + REST operation id: ``list_compartments``, ``get_autonomous_database``). +- Auth flows through ``~/.oci/config`` profiles. ``auth_type`` accepts + ``api_key`` (default), ``security_token`` (session-token profiles + from ``oci session authenticate``), ``instance_principal`` (running + on an OCI compute instance with a dynamic group), or + ``resource_principal`` (running as an OCI Function). + +Run it:: + + # 1. Pick a tenancy-side profile for the OCI control-plane calls + export OCI_USE_PROFILE=API_FREE_TIER # or any other profile in ~/.oci/config + export OCI_USE_REGION=ca-toronto-1 # whichever region you have resources in + export OCI_USE_TENANCY=ocid1.tenancy.oc1... # your tenancy OCID (for compartment-scoped reads) + + # 2. Pick a GenAI-side profile for the model driving the agent + export OCI_GENAI_PROFILE=LUIGI_FRA_API # any profile with OCI GenAI access + export OCI_GENAI_REGION=us-chicago-1 + + python examples/notebook_70_oci_tools.py + python examples/notebook_70_oci_tools.py --part introspection # just describe_oci + python examples/notebook_70_oci_tools.py --part execute # just use_oci + python examples/notebook_70_oci_tools.py --part agent # just the agent loop + +Difficulty: Intermediate + +## Source + +```python +--8<-- "examples/notebook_70_oci_tools.py" +``` diff --git a/docs/workbench.md b/docs/workbench.md index ba4381f3..f65cd156 100644 --- a/docs/workbench.md +++ b/docs/workbench.md @@ -29,19 +29,27 @@ your browser. The catalog leads with the **Oracle primitives** category. Two runnable demos pinned to the top of the sidebar: +- **Drive any OCI service from natural language** — the agent picks + up `describe_oci` + `use_oci` and dispatches against any of the + ~190 OCI Python SDK service modules with one open-spec primitive. + Read-only by default; opt in to mutations with + `LOCUS_USE_OCI_ALLOW_MUTATIONS=1`. Provider panel set to OCI is + enough. See [notebook 70](notebooks/notebook_70_oci_tools.md) and + the [open-spec built-ins section](concepts/tools.md#open-spec-built-ins-for-the-oracle-estate). - **Oracle 26ai RAG (native VECTOR)** — `OracleVectorStore` against an Autonomous Database wallet, native `VECTOR(1024, FLOAT32)` + `VECTOR_DISTANCE COSINE`. Requires `ORACLE_DSN` / `ORACLE_USER` / `ORACLE_PASSWORD` / `ORACLE_WALLET` on the backend host (plus an OCI provider in the UI for embeddings). See - [notebook 06](notebooks/notebook_06_oracle_26ai_rag.md). + [notebook 41](notebooks/notebook_41_oracle_26ai_rag.md). - **Retrieve-then-rerank (Cohere V4)** — `CohereReranker` on OCI on-demand `rerank-v4`. Provider panel set to OCI is enough. See [notebook 05](notebooks/notebook_05_cohere_reranker.md). The notebook sidebar also surfaces the rest of the Oracle-native path — notebooks 01 / 02 / 03 (transports), 04 (Dedicated AI -Cluster) — under the same "Oracle primitives" group. +Cluster), and 41-44 (Oracle 26ai RAG, ADB document loader, in-DB +chunker / embeddings) — under the same "Oracle primitives" group. ### Database settings (Oracle 26ai) @@ -179,7 +187,7 @@ make web # pane 3 — :5173 end-to-end test suite in `workbench/e2e/`. The `make backend` target is the workbench runner — distinct from `make backend-research` and `make backend-finance`, which spin up the A2A mesh demo peers for -[notebook 28](notebooks/notebook_34_a2a_protocol.md), not the +[notebook 28](notebooks/notebook_28_a2a_protocol.md), not the workbench. ### Verify it's up @@ -391,7 +399,7 @@ correlate findings. → specialist_fanout ``` -See [notebook 34](notebooks/notebook_40_emergent_routing.md) for the +See [notebook 34](notebooks/notebook_34_emergent_routing.md) for the full code path and [concepts/router.md](concepts/router.md#emergent-picker-opt-in-second-mode) for the architectural details. diff --git a/examples/notebook_14_basic_agent.py b/examples/notebook_06_basic_agent.py similarity index 100% rename from examples/notebook_14_basic_agent.py rename to examples/notebook_06_basic_agent.py diff --git a/examples/notebook_15_agent_with_tools.py b/examples/notebook_07_agent_with_tools.py similarity index 100% rename from examples/notebook_15_agent_with_tools.py rename to examples/notebook_07_agent_with_tools.py diff --git a/examples/notebook_16_agent_memory.py b/examples/notebook_08_agent_memory.py similarity index 100% rename from examples/notebook_16_agent_memory.py rename to examples/notebook_08_agent_memory.py diff --git a/examples/notebook_13_oracle_agent_memory.py b/examples/notebook_09_oracle_agent_memory.py similarity index 99% rename from examples/notebook_13_oracle_agent_memory.py rename to examples/notebook_09_oracle_agent_memory.py index e3b49784..e7d69548 100755 --- a/examples/notebook_13_oracle_agent_memory.py +++ b/examples/notebook_09_oracle_agent_memory.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 13: a Locus agent with Oracle's official agent-memory client. +"""Notebook 09: a Locus agent with Oracle's official agent-memory client. The Oracle-native memory path. Wires Locus's :class:`~locus.memory.managers.OracleAgentMemoryManager` to diff --git a/examples/notebook_11_oracle_store.py b/examples/notebook_10_oracle_store.py similarity index 99% rename from examples/notebook_11_oracle_store.py rename to examples/notebook_10_oracle_store.py index a4312c8a..54646c57 100755 --- a/examples/notebook_11_oracle_store.py +++ b/examples/notebook_10_oracle_store.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 11: a Locus agent with cross-thread memory on Oracle 26ai. +"""Notebook 10: a Locus agent with cross-thread memory on Oracle 26ai. ``oracle_checkpointer`` (notebook 07) persists *per-thread* agent state — same ``thread_id`` resumes a conversation. :class:`OracleStore` diff --git a/examples/notebook_17_agent_streaming.py b/examples/notebook_11_agent_streaming.py similarity index 100% rename from examples/notebook_17_agent_streaming.py rename to examples/notebook_11_agent_streaming.py diff --git a/examples/notebook_18_agent_hooks.py b/examples/notebook_12_agent_hooks.py similarity index 100% rename from examples/notebook_18_agent_hooks.py rename to examples/notebook_12_agent_hooks.py diff --git a/examples/notebook_19_sse_streaming.py b/examples/notebook_13_sse_streaming.py similarity index 100% rename from examples/notebook_19_sse_streaming.py rename to examples/notebook_13_sse_streaming.py diff --git a/examples/notebook_20_hooks_advanced.py b/examples/notebook_14_hooks_advanced.py similarity index 100% rename from examples/notebook_20_hooks_advanced.py rename to examples/notebook_14_hooks_advanced.py diff --git a/examples/notebook_21_termination.py b/examples/notebook_15_termination.py similarity index 100% rename from examples/notebook_21_termination.py rename to examples/notebook_15_termination.py diff --git a/examples/notebook_22_basic_graph.py b/examples/notebook_16_basic_graph.py similarity index 100% rename from examples/notebook_22_basic_graph.py rename to examples/notebook_16_basic_graph.py diff --git a/examples/notebook_23_conditional_routing.py b/examples/notebook_17_conditional_routing.py similarity index 100% rename from examples/notebook_23_conditional_routing.py rename to examples/notebook_17_conditional_routing.py diff --git a/examples/notebook_24_state_reducers.py b/examples/notebook_18_state_reducers.py similarity index 100% rename from examples/notebook_24_state_reducers.py rename to examples/notebook_18_state_reducers.py diff --git a/examples/notebook_25_human_in_the_loop.py b/examples/notebook_19_human_in_the_loop.py similarity index 100% rename from examples/notebook_25_human_in_the_loop.py rename to examples/notebook_19_human_in_the_loop.py diff --git a/examples/notebook_26_advanced_patterns.py b/examples/notebook_20_advanced_patterns.py similarity index 100% rename from examples/notebook_26_advanced_patterns.py rename to examples/notebook_20_advanced_patterns.py diff --git a/examples/notebook_27_composition.py b/examples/notebook_21_composition.py similarity index 100% rename from examples/notebook_27_composition.py rename to examples/notebook_21_composition.py diff --git a/examples/notebook_28_graph_advanced.py b/examples/notebook_22_graph_advanced.py similarity index 100% rename from examples/notebook_28_graph_advanced.py rename to examples/notebook_22_graph_advanced.py diff --git a/examples/notebook_29_functional_api.py b/examples/notebook_23_functional_api.py similarity index 100% rename from examples/notebook_29_functional_api.py rename to examples/notebook_23_functional_api.py diff --git a/examples/notebook_30_swarm_multiagent.py b/examples/notebook_24_swarm_multiagent.py similarity index 100% rename from examples/notebook_30_swarm_multiagent.py rename to examples/notebook_24_swarm_multiagent.py diff --git a/examples/notebook_31_agent_handoff.py b/examples/notebook_25_agent_handoff.py similarity index 100% rename from examples/notebook_31_agent_handoff.py rename to examples/notebook_25_agent_handoff.py diff --git a/examples/notebook_32_orchestrator_pattern.py b/examples/notebook_26_orchestrator_pattern.py similarity index 100% rename from examples/notebook_32_orchestrator_pattern.py rename to examples/notebook_26_orchestrator_pattern.py diff --git a/examples/notebook_33_specialist_agents.py b/examples/notebook_27_specialist_agents.py similarity index 100% rename from examples/notebook_33_specialist_agents.py rename to examples/notebook_27_specialist_agents.py diff --git a/examples/notebook_34_a2a_protocol.py b/examples/notebook_28_a2a_protocol.py similarity index 100% rename from examples/notebook_34_a2a_protocol.py rename to examples/notebook_28_a2a_protocol.py diff --git a/examples/notebook_35_deepagent.py b/examples/notebook_29_deepagent.py similarity index 100% rename from examples/notebook_35_deepagent.py rename to examples/notebook_29_deepagent.py diff --git a/examples/notebook_36_map_reduce_code_review.py b/examples/notebook_30_map_reduce_code_review.py similarity index 100% rename from examples/notebook_36_map_reduce_code_review.py rename to examples/notebook_30_map_reduce_code_review.py diff --git a/examples/notebook_37_supervisor_critic_loop.py b/examples/notebook_31_supervisor_critic_loop.py similarity index 100% rename from examples/notebook_37_supervisor_critic_loop.py rename to examples/notebook_31_supervisor_critic_loop.py diff --git a/examples/notebook_38_debate_with_judge.py b/examples/notebook_32_debate_with_judge.py similarity index 100% rename from examples/notebook_38_debate_with_judge.py rename to examples/notebook_32_debate_with_judge.py diff --git a/examples/notebook_39_multiagent_human_in_loop.py b/examples/notebook_33_multiagent_human_in_loop.py similarity index 100% rename from examples/notebook_39_multiagent_human_in_loop.py rename to examples/notebook_33_multiagent_human_in_loop.py diff --git a/examples/notebook_40_emergent_routing.py b/examples/notebook_34_emergent_routing.py similarity index 100% rename from examples/notebook_40_emergent_routing.py rename to examples/notebook_34_emergent_routing.py diff --git a/examples/notebook_41_structured_output.py b/examples/notebook_35_structured_output.py similarity index 100% rename from examples/notebook_41_structured_output.py rename to examples/notebook_35_structured_output.py diff --git a/examples/notebook_42_reasoning_patterns.py b/examples/notebook_36_reasoning_patterns.py similarity index 100% rename from examples/notebook_42_reasoning_patterns.py rename to examples/notebook_36_reasoning_patterns.py diff --git a/examples/notebook_43_gsar_typed_grounding.py b/examples/notebook_37_gsar_typed_grounding.py similarity index 97% rename from examples/notebook_43_gsar_typed_grounding.py rename to examples/notebook_37_gsar_typed_grounding.py index a480f19f..dbd9d542 100644 --- a/examples/notebook_43_gsar_typed_grounding.py +++ b/examples/notebook_37_gsar_typed_grounding.py @@ -4,9 +4,10 @@ """Notebook 38: GSAR — typed grounding for hallucination detection. GSAR (Grounded Structured Answer Reasoning) is the Locus layer from -``arXiv:2604.23366`` (Kamelhar 2026). It partitions an answer's claims -into four buckets, scores them against evidence, and decides whether -to proceed, regenerate, or replan. +`Federico A. Kamelhar (2026), arXiv:2604.23366 `_. +It partitions an answer's claims into four buckets, scores them +against evidence, and decides whether to proceed, regenerate, or +replan. - The four-way partition — grounded / ungrounded / contradicted / complementary — as a Pydantic type. diff --git a/examples/notebook_44_rag_basics.py b/examples/notebook_38_rag_basics.py similarity index 100% rename from examples/notebook_44_rag_basics.py rename to examples/notebook_38_rag_basics.py diff --git a/examples/notebook_45_rag_providers.py b/examples/notebook_39_rag_providers.py similarity index 100% rename from examples/notebook_45_rag_providers.py rename to examples/notebook_39_rag_providers.py diff --git a/examples/notebook_46_rag_agents.py b/examples/notebook_40_rag_agents.py similarity index 100% rename from examples/notebook_46_rag_agents.py rename to examples/notebook_40_rag_agents.py diff --git a/examples/notebook_06_oracle_26ai_rag.py b/examples/notebook_41_oracle_26ai_rag.py similarity index 99% rename from examples/notebook_06_oracle_26ai_rag.py rename to examples/notebook_41_oracle_26ai_rag.py index 4eba7330..aa8b4dc3 100755 --- a/examples/notebook_06_oracle_26ai_rag.py +++ b/examples/notebook_41_oracle_26ai_rag.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 06: a Locus agent that retrieves over Oracle Database 26ai. +"""Notebook 41: a Locus agent that retrieves over Oracle Database 26ai. The Locus agent runtime is the protagonist here. Oracle 26ai is the durable substrate — embeddings live in a native ``VECTOR(N, FLOAT32)`` diff --git a/examples/notebook_08_oracle_adb_loader.py b/examples/notebook_42_oracle_adb_loader.py similarity index 99% rename from examples/notebook_08_oracle_adb_loader.py rename to examples/notebook_42_oracle_adb_loader.py index 5b58dc43..59288921 100755 --- a/examples/notebook_08_oracle_adb_loader.py +++ b/examples/notebook_42_oracle_adb_loader.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 08: a Locus agent that pulls rows from Autonomous Database. +"""Notebook 42: a Locus agent that pulls rows from Autonomous Database. The first link of any Oracle-native data flow is "get the source rows out of the database". Locus ships diff --git a/examples/notebook_09_oracle_indb_chunker.py b/examples/notebook_43_oracle_indb_chunker.py similarity index 99% rename from examples/notebook_09_oracle_indb_chunker.py rename to examples/notebook_43_oracle_indb_chunker.py index eed71f0b..1bce5e32 100755 --- a/examples/notebook_09_oracle_indb_chunker.py +++ b/examples/notebook_43_oracle_indb_chunker.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 09: a Locus agent driving an in-database chunker. +"""Notebook 43: a Locus agent driving an in-database chunker. Oracle 23ai / 26ai ships a server-side chunking primitive, ``DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS``. It tokenises and segments text diff --git a/examples/notebook_10_oracle_indb_embeddings.py b/examples/notebook_44_oracle_indb_embeddings.py similarity index 99% rename from examples/notebook_10_oracle_indb_embeddings.py rename to examples/notebook_44_oracle_indb_embeddings.py index 3b94a05a..ed000229 100755 --- a/examples/notebook_10_oracle_indb_embeddings.py +++ b/examples/notebook_44_oracle_indb_embeddings.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 10: a Locus agent doing data-residency-aware RAG in-database. +"""Notebook 44: a Locus agent doing data-residency-aware RAG in-database. Oracle 23ai / 26ai can host ONNX embedding models *inside* the database via ``DBMS_VECTOR.LOAD_ONNX_MODEL``. When the embedding model diff --git a/examples/notebook_47_mcp_integration.py b/examples/notebook_45_mcp_integration.py similarity index 100% rename from examples/notebook_47_mcp_integration.py rename to examples/notebook_45_mcp_integration.py diff --git a/examples/notebook_48_playbooks.py b/examples/notebook_46_playbooks.py similarity index 100% rename from examples/notebook_48_playbooks.py rename to examples/notebook_46_playbooks.py diff --git a/examples/notebook_49_plugins.py b/examples/notebook_47_plugins.py similarity index 100% rename from examples/notebook_49_plugins.py rename to examples/notebook_47_plugins.py diff --git a/examples/notebook_50_skills.py b/examples/notebook_48_skills.py similarity index 100% rename from examples/notebook_50_skills.py rename to examples/notebook_48_skills.py diff --git a/examples/notebook_51_steering.py b/examples/notebook_49_steering.py similarity index 100% rename from examples/notebook_51_steering.py rename to examples/notebook_49_steering.py diff --git a/examples/notebook_52_guardrails_security.py b/examples/notebook_50_guardrails_security.py similarity index 100% rename from examples/notebook_52_guardrails_security.py rename to examples/notebook_50_guardrails_security.py diff --git a/examples/notebook_53_guardrails_advanced.py b/examples/notebook_51_guardrails_advanced.py similarity index 100% rename from examples/notebook_53_guardrails_advanced.py rename to examples/notebook_51_guardrails_advanced.py diff --git a/examples/notebook_54_checkpoint_backends.py b/examples/notebook_52_checkpoint_backends.py similarity index 100% rename from examples/notebook_54_checkpoint_backends.py rename to examples/notebook_52_checkpoint_backends.py diff --git a/examples/notebook_07_oracle_26ai_checkpointer.py b/examples/notebook_53_oracle_26ai_checkpointer.py similarity index 99% rename from examples/notebook_07_oracle_26ai_checkpointer.py rename to examples/notebook_53_oracle_26ai_checkpointer.py index 253615bc..d678f97b 100755 --- a/examples/notebook_07_oracle_26ai_checkpointer.py +++ b/examples/notebook_53_oracle_26ai_checkpointer.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 07: durable agent threads on Oracle Database 26ai. +"""Notebook 53: durable agent threads on Oracle Database 26ai. In-memory and on-disk checkpointers are fine for local development — when the process restarts, the conversation is gone. This notebook diff --git a/examples/notebook_12_oracle_versioned_saver.py b/examples/notebook_54_oracle_versioned_saver.py similarity index 99% rename from examples/notebook_12_oracle_versioned_saver.py rename to examples/notebook_54_oracle_versioned_saver.py index 14782256..3ed98433 100755 --- a/examples/notebook_12_oracle_versioned_saver.py +++ b/examples/notebook_54_oracle_versioned_saver.py @@ -3,7 +3,7 @@ # Licensed under the Universal Permissive License v1.0 as shown at # https://oss.oracle.com/licenses/upl/ -"""Notebook 12: versioned agent history on Oracle Database 26ai. +"""Notebook 54: versioned agent history on Oracle Database 26ai. Notebook 07's ``oracle_checkpointer`` keeps **one row per thread** — ``MERGE`` on save means the latest write wins and history is diff --git a/examples/notebook_70_oci_tools.py b/examples/notebook_70_oci_tools.py new file mode 100644 index 00000000..76c31635 --- /dev/null +++ b/examples/notebook_70_oci_tools.py @@ -0,0 +1,329 @@ +# Copyright (c) 2025, 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v1.0 as shown at +# https://oss.oracle.com/licenses/upl/ +"""Notebook 70: Drive the entire OCI control plane from natural language — `use_oci` + `describe_oci`. + +Locus ships two built-in tools that, together, expose every OCI Python +SDK operation to an agent without per-service plumbing: + +- ``describe_oci(service?, client?, operation?)`` — runtime + introspection. Progressively zooms into the SDK: list services + (~190), list clients in a service, list operations on a client, + return the parameter schema for one operation (parsed from the + OCI SDK docstring). +- ``use_oci(service, client, operation, parameters, ...)`` — execute + any introspected call. Builds the right ``oci..`` + with the requested auth, invokes the method, and serialises the + response (snake_case Python attrs are rendered with their wire + camelCase names via ``attribute_map``). + +The agent's loop is: hand the model the two tools, the model picks +``service`` + ``client`` + ``operation`` from natural-language intent, +optionally calls ``describe_oci`` to verify the parameter shape, then +calls ``use_oci`` to run it. One open-spec primitive covers the +entire OCI surface — Identity, Compute, Database, Object Storage, +Networking, Vault, Functions, Bastion, GenAI, the lot. + +Safety: ``use_oci`` is read-only by default. Operations whose names +don't start with a read-only prefix (``list_``, ``get_``, ``head_``, +``summarize_``, ``describe_``, ``search_``, ``fetch_``, ``compute_``, +``preview_``, ``validate_``, ``test_``) are refused unless either +``allow_mutations=True`` is passed explicitly or the env var +``LOCUS_USE_OCI_ALLOW_MUTATIONS=1`` is set. + +Key concepts: + +- A *service* is an OCI Python SDK submodule under ``oci.*`` + (snake_case: ``identity``, ``core``, ``database``, ``object_storage``). +- A *client* is a class inside the service module (CamelCase, ends in + ``Client``: ``IdentityClient``, ``ComputeClient``, ``DatabaseClient``). +- An *operation* is a method on the client (snake_case, mirrors the + REST operation id: ``list_compartments``, ``get_autonomous_database``). +- Auth flows through ``~/.oci/config`` profiles. ``auth_type`` accepts + ``api_key`` (default), ``security_token`` (session-token profiles + from ``oci session authenticate``), ``instance_principal`` (running + on an OCI compute instance with a dynamic group), or + ``resource_principal`` (running as an OCI Function). + +Run it:: + + # 1. Pick a tenancy-side profile for the OCI control-plane calls + export OCI_USE_PROFILE=API_FREE_TIER # or any other profile in ~/.oci/config + export OCI_USE_REGION=ca-toronto-1 # whichever region you have resources in + export OCI_USE_TENANCY=ocid1.tenancy.oc1... # your tenancy OCID (for compartment-scoped reads) + + # 2. Pick a GenAI-side profile for the model driving the agent + export OCI_GENAI_PROFILE=LUIGI_FRA_API # any profile with OCI GenAI access + export OCI_GENAI_REGION=us-chicago-1 + + python examples/notebook_70_oci_tools.py + python examples/notebook_70_oci_tools.py --part introspection # just describe_oci + python examples/notebook_70_oci_tools.py --part execute # just use_oci + python examples/notebook_70_oci_tools.py --part agent # just the agent loop + +Difficulty: Intermediate +""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import sys +from typing import Any + + +def _env(name: str, default: str | None = None) -> str: + val = os.environ.get(name, default) + if not val: + sys.stderr.write( + f"missing env var {name} — see the prerequisites in the notebook docstring\n" + ) + sys.exit(2) + return val + + +# ============================================================================= +# Part 1 — describe_oci: walk the SDK at four levels +# ============================================================================= + + +async def part1_introspection() -> None: + """Show every level of API discovery, no network calls required.""" + from locus.tools import describe_oci + + print("=== describe_oci — runtime introspection ===\n") + + # Level 1: every service module under oci.* (~190 of them) + services = json.loads(await describe_oci.execute()) + print(f"Level 1 — services: {services['count']} modules") + print(f" sample: {services['services'][:6]} ...\n") + + # Level 2: clients inside a service + core = json.loads(await describe_oci.execute(service="core")) + print(f"Level 2 — clients in 'core': {core['clients']}\n") + + # Level 3: operations on one client, partitioned by read/write + ops = json.loads( + await describe_oci.execute(service="object_storage", client="ObjectStorageClient") + ) + print(f"Level 3 — operations on ObjectStorageClient: {ops['count']} total") + print(f" read-only (first 5): {ops['read_only_operations'][:5]}") + print(f" mutating (first 5): {ops['mutating_operations'][:5]}\n") + + # Level 4: parameter schema for one operation, parsed from docstring + schema = json.loads( + await describe_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + ) + ) + print(f"Level 4 — schema for {schema['signature']}") + print(f" read_only: {schema['read_only']}") + print(f" required params: {schema['required_parameters']}") + print(" all params (truncated):") + for p in schema["parameters"][:6]: + flag = "required" if p["required"] else "optional" + print(f" - {p['name']:32s} {p['type']:10s} {flag:8s} {p['description'][:60]}") + print() + + +# ============================================================================= +# Part 2 — use_oci: dispatch any operation, with safety gating +# ============================================================================= + + +async def part2_execute() -> None: + """Call real OCI services directly through use_oci.""" + from locus.tools import use_oci + + profile = _env("OCI_USE_PROFILE") + region = _env("OCI_USE_REGION") + tenancy = _env("OCI_USE_TENANCY") + + print(f"=== use_oci — direct dispatch (profile={profile}, region={region}) ===\n") + + # Identity: list every compartment in the tenancy subtree + result = json.loads( + await use_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + parameters={"compartment_id_in_subtree": True}, + compartment_id=tenancy, + profile=profile, + region=region, + label="list every compartment in the tenancy", + ) + ) + print( + f"identity.list_compartments → http {result.get('http_status')} " + f"opc-request-id={result.get('opc_request_id', '')[:32]}..." + ) + print(f" found {len(result.get('data', []))} compartment(s)") + for c in result.get("data", [])[:3]: + print(f" - {c.get('name'):30s} {c.get('lifecycleState')}") + print() + + # Object Storage: get the tenancy's namespace (an unauth'd-looking call + # that still uses your signer). + result = json.loads( + await use_oci.execute( + service="object_storage", + client="ObjectStorageClient", + operation="get_namespace", + parameters={}, + profile=profile, + region=region, + ) + ) + print( + f"object_storage.get_namespace → http {result.get('http_status')} " + f"namespace='{result.get('data')}'\n" + ) + + # Safety gate: a mutating call is refused without the opt-in. + result = json.loads( + await use_oci.execute( + service="object_storage", + client="ObjectStorageClient", + operation="delete_bucket", + parameters={"namespace_name": "nope", "bucket_name": "nope"}, + profile=profile, + region=region, + ) + ) + print("Safety: delete_bucket without allow_mutations →") + print(f" status='{result['status']}' error='{result['error'][:90]}...'\n") + + +# ============================================================================= +# Part 3 — Agent loop: NL → describe_oci → use_oci → English answer +# ============================================================================= + + +def _system_prompt(tenancy: str, profile: str) -> str: + return ( + f"You are an OCI cloud assistant. The user's profile is " + f"'{profile}' (api_key auth). Their tenancy OCID is:\n" + f"{tenancy}\n\n" + f"You have two tools:\n" + f"1. `describe_oci(service?, client?, operation?)` — introspect the OCI " + f"Python SDK. Call this first whenever you're unsure which " + f"service/client/operation/parameters to use. It progressively narrows:\n" + f" - no args → list every service module under oci.*\n" + f" - service → list every *Client class in that service\n" + f" - service + client → list every operation on that client\n" + f" - service + client + operation → return parameter schema\n" + f"2. `use_oci(service, client, operation, parameters, ...)` — execute " + f"the call. Always pass profile='{profile}'. Use compartment_id= for tenancy-scoped reads.\n\n" + f"OCI Python SDK conventions:\n" + f"- service = snake_case module name ('identity', 'core', 'database')\n" + f"- client = CamelCase class ('IdentityClient', 'ComputeClient')\n" + f"- operation = snake_case method ('list_compartments', 'get_instance')\n\n" + f"Discover before executing. Answer in plain English." + ) + + +async def part3_agent() -> None: + """Watch the agent self-discover the SDK and answer free-form questions.""" + from locus.agent import Agent + from locus.models import get_model + from locus.tools import describe_oci, use_oci + + use_profile = _env("OCI_USE_PROFILE") + use_region = _env("OCI_USE_REGION") + tenancy = _env("OCI_USE_TENANCY") + genai_profile = _env("OCI_GENAI_PROFILE") + genai_region = _env("OCI_GENAI_REGION", "us-chicago-1") + + print( + f"=== Agent loop (model via {genai_profile}@{genai_region}, " + f"OCI calls via {use_profile}@{use_region}) ===\n" + ) + + model = get_model("oci:openai.gpt-4o", profile=genai_profile, region=genai_region) + agent = Agent( + model=model, + tools=[describe_oci, use_oci], + system_prompt=_system_prompt(tenancy, use_profile), + max_iterations=12, + ) + + prompts = [ + "What regions am I subscribed to?", + "Are there any compute instances running anywhere in my tenancy?", + ] + + for prompt in prompts: + print(f"USER: {prompt}") + result = agent.run_sync(prompt) + print(f"AGENT: {result.message.strip()}") + # Show the call sequence the model picked — proof it self-discovered. + calls: list[str] = [] + for msg in result.state.messages: + for tc in msg.tool_calls or []: + args: Any = tc.arguments if isinstance(tc.arguments, dict) else {} + if tc.name == "use_oci": + calls.append( + f" -> use_oci({args.get('service')}.{args.get('client')}." + f"{args.get('operation')}, region={args.get('region')})" + ) + elif tc.name == "describe_oci": + bits = [ + f"{k}={args.get(k)}" + for k in ("service", "client", "operation") + if args.get(k) + ] + calls.append(f" -> describe_oci({', '.join(bits)})") + if calls: + print("Tool-call sequence:") + print("\n".join(calls)) + print() + + +# ============================================================================= +# Why this is a great primitive +# ============================================================================= +# +# Adding a new OCI resource type to your agent normally means: +# +# 1. Read the OCI Python SDK docs. +# 2. Write a thin @tool wrapper around the right ``oci..`` +# method, with the right ``compartment_id`` plumbing, the right +# paginator, the right serialiser. +# 3. Register that tool with the agent. +# 4. Repeat for every operation you want to expose (~190 services x N ops). +# +# With ``use_oci`` + ``describe_oci``: +# +# 1. Hand the model the two tools. Done. +# +# The model uses ``describe_oci`` like a man page — discovers what +# operations exist, what parameters they need — and then calls +# ``use_oci`` to execute. There is exactly one execution path, with one +# auth code-path, one serialiser, one mutation-safety gate. Adding +# coverage for a new service does not require writing any new code. +# ============================================================================= + + +async def main(part: str) -> None: + if part in {"all", "introspection"}: + await part1_introspection() + if part in {"all", "execute"}: + await part2_execute() + if part in {"all", "agent"}: + await part3_agent() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--part", + choices=["all", "introspection", "execute", "agent"], + default="all", + ) + asyncio.run(main(parser.parse_args().part)) diff --git a/mkdocs.yml b/mkdocs.yml index 44c96a3a..5d0ea35f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -188,63 +188,62 @@ nav: - 3 · OCI Responses: notebooks/notebook_03_oci_responses.md - 4 · OCI Dedicated AI Cluster: notebooks/notebook_04_oci_dac.md - 5 · Cohere Reranker V4 on OCI: notebooks/notebook_05_cohere_reranker.md - - Oracle Database 26ai: - - 6 · Oracle 26ai RAG: notebooks/notebook_06_oracle_26ai_rag.md - - 7 · Oracle 26ai checkpointer: notebooks/notebook_07_oracle_26ai_checkpointer.md - - 8 · Oracle ADB document loader: notebooks/notebook_08_oracle_adb_loader.md - - 9 · Oracle in-DB chunker: notebooks/notebook_09_oracle_indb_chunker.md - - 10 · Oracle in-DB embeddings: notebooks/notebook_10_oracle_indb_embeddings.md - - 11 · Oracle store (portable BaseStore path): notebooks/notebook_11_oracle_store.md - - 12 · Oracle versioned checkpoint saver: notebooks/notebook_12_oracle_versioned_saver.md - - 13 · Oracle agent memory: notebooks/notebook_13_oracle_agent_memory.md - Agent Foundations: - - 14 · Basic agent: notebooks/notebook_14_basic_agent.md - - 15 · Agent with tools: notebooks/notebook_15_agent_with_tools.md - - 16 · Conversation memory: notebooks/notebook_16_agent_memory.md - - 17 · Streaming events: notebooks/notebook_17_agent_streaming.md - - 18 · Lifecycle hooks: notebooks/notebook_18_agent_hooks.md - - 19 · SSE streaming: notebooks/notebook_19_sse_streaming.md - - 20 · Hooks: notebooks/notebook_20_hooks_advanced.md - - 21 · Termination conditions: notebooks/notebook_21_termination.md + - 6 · Basic agent: notebooks/notebook_06_basic_agent.md + - 7 · Agent with tools: notebooks/notebook_07_agent_with_tools.md + - 8 · Conversation memory: notebooks/notebook_08_agent_memory.md + - 9 · Oracle agent memory: notebooks/notebook_09_oracle_agent_memory.md + - 10 · Oracle store (portable BaseStore path): notebooks/notebook_10_oracle_store.md + - 11 · Streaming events: notebooks/notebook_11_agent_streaming.md + - 12 · Lifecycle hooks: notebooks/notebook_12_agent_hooks.md + - 13 · SSE streaming: notebooks/notebook_13_sse_streaming.md + - 14 · Hooks: notebooks/notebook_14_hooks_advanced.md + - 15 · Termination conditions: notebooks/notebook_15_termination.md - Graphs & composition: - - 22 · Basic graph: notebooks/notebook_22_basic_graph.md - - 23 · Conditional routing: notebooks/notebook_23_conditional_routing.md - - 24 · State reducers: notebooks/notebook_24_state_reducers.md - - 25 · Human-in-the-loop: notebooks/notebook_25_human_in_the_loop.md - - 26 · Command + advanced patterns: notebooks/notebook_26_advanced_patterns.md - - 27 · Composition: notebooks/notebook_27_composition.md - - 28 · Graph: notebooks/notebook_28_graph_advanced.md - - 29 · Functional API: notebooks/notebook_29_functional_api.md + - 16 · Basic graph: notebooks/notebook_16_basic_graph.md + - 17 · Conditional routing: notebooks/notebook_17_conditional_routing.md + - 18 · State reducers: notebooks/notebook_18_state_reducers.md + - 19 · Human-in-the-loop: notebooks/notebook_19_human_in_the_loop.md + - 20 · Command + advanced patterns: notebooks/notebook_20_advanced_patterns.md + - 21 · Composition: notebooks/notebook_21_composition.md + - 22 · Graph: notebooks/notebook_22_graph_advanced.md + - 23 · Functional API: notebooks/notebook_23_functional_api.md - Multi-agent: - - 30 · Swarm: notebooks/notebook_30_swarm_multiagent.md - - 31 · Agent handoff: notebooks/notebook_31_agent_handoff.md - - 32 · Orchestrator pattern: notebooks/notebook_32_orchestrator_pattern.md - - 33 · Specialist agents: notebooks/notebook_33_specialist_agents.md - - 34 · A2A protocol: notebooks/notebook_34_a2a_protocol.md - - 35 · DeepAgent: notebooks/notebook_35_deepagent.md - - 36 · Map-reduce code review: notebooks/notebook_36_map_reduce_code_review.md - - 37 · Supervisor + critic loop: notebooks/notebook_37_supervisor_critic_loop.md - - 38 · Adversarial debate + judge: notebooks/notebook_38_debate_with_judge.md - - 39 · Multi-agent + human-in-the-loop: notebooks/notebook_39_multiagent_human_in_loop.md - - 40 · Emergent routing: notebooks/notebook_40_emergent_routing.md + - 24 · Swarm: notebooks/notebook_24_swarm_multiagent.md + - 25 · Agent handoff: notebooks/notebook_25_agent_handoff.md + - 26 · Orchestrator pattern: notebooks/notebook_26_orchestrator_pattern.md + - 27 · Specialist agents: notebooks/notebook_27_specialist_agents.md + - 28 · A2A protocol: notebooks/notebook_28_a2a_protocol.md + - 29 · DeepAgent: notebooks/notebook_29_deepagent.md + - 30 · Map-reduce code review: notebooks/notebook_30_map_reduce_code_review.md + - 31 · Supervisor + critic loop: notebooks/notebook_31_supervisor_critic_loop.md + - 32 · Adversarial debate + judge: notebooks/notebook_32_debate_with_judge.md + - 33 · Multi-agent + human-in-the-loop: notebooks/notebook_33_multiagent_human_in_loop.md + - 34 · Emergent routing: notebooks/notebook_34_emergent_routing.md - Reasoning & structured output: - - 41 · Structured output: notebooks/notebook_41_structured_output.md - - 42 · Reasoning patterns: notebooks/notebook_42_reasoning_patterns.md - - 43 · GSAR typed grounding: notebooks/notebook_43_gsar_typed_grounding.md + - 35 · Structured output: notebooks/notebook_35_structured_output.md + - 36 · Reasoning patterns: notebooks/notebook_36_reasoning_patterns.md + - 37 · GSAR typed grounding: notebooks/notebook_37_gsar_typed_grounding.md - RAG: - - 44 · RAG basics: notebooks/notebook_44_rag_basics.md - - 45 · RAG providers: notebooks/notebook_45_rag_providers.md - - 46 · RAG agents: notebooks/notebook_46_rag_agents.md + - 38 · RAG basics: notebooks/notebook_38_rag_basics.md + - 39 · RAG providers: notebooks/notebook_39_rag_providers.md + - 40 · RAG agents: notebooks/notebook_40_rag_agents.md + - 41 · Oracle 26ai RAG: notebooks/notebook_41_oracle_26ai_rag.md + - 42 · Oracle ADB document loader: notebooks/notebook_42_oracle_adb_loader.md + - 43 · Oracle in-DB chunker: notebooks/notebook_43_oracle_indb_chunker.md + - 44 · Oracle in-DB embeddings: notebooks/notebook_44_oracle_indb_embeddings.md - Skills, playbooks & plugins: - - 47 · MCP integration: notebooks/notebook_47_mcp_integration.md - - 48 · Playbooks: notebooks/notebook_48_playbooks.md - - 49 · Plugins: notebooks/notebook_49_plugins.md - - 50 · Skills: notebooks/notebook_50_skills.md - - 51 · Steering: notebooks/notebook_51_steering.md + - 45 · MCP integration: notebooks/notebook_45_mcp_integration.md + - 46 · Playbooks: notebooks/notebook_46_playbooks.md + - 47 · Plugins: notebooks/notebook_47_plugins.md + - 48 · Skills: notebooks/notebook_48_skills.md + - 49 · Steering: notebooks/notebook_49_steering.md - Production: - - 52 · Guardrails & security: notebooks/notebook_52_guardrails_security.md - - 53 · Guardrails: notebooks/notebook_53_guardrails_advanced.md - - 54 · Checkpoint backends: notebooks/notebook_54_checkpoint_backends.md + - 50 · Guardrails & security: notebooks/notebook_50_guardrails_security.md + - 51 · Guardrails: notebooks/notebook_51_guardrails_advanced.md + - 52 · Checkpoint backends: notebooks/notebook_52_checkpoint_backends.md + - 53 · Oracle 26ai checkpointer: notebooks/notebook_53_oracle_26ai_checkpointer.md + - 54 · Oracle versioned checkpoint saver: notebooks/notebook_54_oracle_versioned_saver.md - 55 · Evaluation: notebooks/notebook_55_evaluation.md - 56 · Model providers: notebooks/notebook_56_model_providers.md - 57 · Multi-modal providers: notebooks/notebook_57_multimodal_providers.md @@ -263,6 +262,7 @@ nav: - Server & full pipelines: - 68 · Agent server: notebooks/notebook_68_agent_server.md - 69 · Research workflow: notebooks/notebook_69_research_workflow.md + - 70 · OCI tools — agents that drive OCI: notebooks/notebook_70_oci_tools.md - Guides: - Deploy: how-to/deploy.md - Persist conversations: how-to/persist-conversations.md diff --git a/src/locus/tools/__init__.py b/src/locus/tools/__init__.py index 56f95366..74451785 100644 --- a/src/locus/tools/__init__.py +++ b/src/locus/tools/__init__.py @@ -4,7 +4,7 @@ """Tool system for Locus.""" -from locus.tools.builtins import get_today_date +from locus.tools.builtins import describe_oci, get_today_date, use_oci from locus.tools.context import ToolContext from locus.tools.decorator import tool from locus.tools.executor import ConcurrentExecutor, SequentialExecutor, ToolExecutor @@ -14,6 +14,7 @@ __all__ = [ "ConcurrentExecutor", + "describe_oci", "SequentialExecutor", "ToolContext", "ToolExecutor", @@ -22,4 +23,5 @@ "get_today_date", "pydantic_to_json_schema", "tool", + "use_oci", ] diff --git a/src/locus/tools/builtins.py b/src/locus/tools/builtins.py index 2d95e04d..9bb9005d 100644 --- a/src/locus/tools/builtins.py +++ b/src/locus/tools/builtins.py @@ -13,6 +13,7 @@ from datetime import datetime, timedelta from locus.tools.decorator import tool +from locus.tools.oci import describe_oci, use_oci @tool(idempotent=True) @@ -54,4 +55,4 @@ def get_today_date() -> dict: } -__all__ = ["get_today_date"] +__all__ = ["describe_oci", "get_today_date", "use_oci"] diff --git a/src/locus/tools/oci.py b/src/locus/tools/oci.py new file mode 100644 index 00000000..85648510 --- /dev/null +++ b/src/locus/tools/oci.py @@ -0,0 +1,642 @@ +# Copyright (c) 2025, 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v1.0 as shown at +# https://oss.oracle.com/licenses/upl/ + +"""``use_oci`` — open-spec dispatch to any OCI Python SDK operation. + +Instead of writing a separate ``@tool`` for every OCI resource (one +for compartments, one for ADBs, one for buckets, …), the agent loop +calls a single tool and the LLM picks the right +``oci...(...)`` call from +natural-language intent. + +Concretely the agent emits a structured tool call of the form:: + + use_oci( + service="identity", + client="IdentityClient", + operation="list_compartments", + parameters={"compartment_id": "ocid1.tenancy.oc1..xxx"}, + profile="DEFAULT", + region="us-ashburn-1", + label="Find every compartment in the tenancy", + ) + +and the tool resolves that into ``oci.identity.IdentityClient(config) +.list_compartments(**parameters)``, serializes the response, and +returns it as JSON. The OCI SDK has ~190 services, so a single +``use_oci`` covers essentially the whole control plane without any +per-service plumbing. + +Safety: read-only by default. Mutating operations (``create_*``, +``update_*``, ``delete_*``, ``change_*``, ``terminate_*``, …) are +refused unless the caller opts in via either the ``allow_mutations`` +parameter or the ``LOCUS_USE_OCI_ALLOW_MUTATIONS=1`` env var. +Enforced in code rather than via an interactive prompt — locus tools +must be non-interactive. +""" + +from __future__ import annotations + +import datetime as _dt +import importlib +import inspect +import json +import os +import re +from decimal import Decimal +from enum import Enum +from pathlib import Path +from typing import Any, cast + +from locus.tools.decorator import tool + + +# OCI SDK methods follow strict naming. Anything *not* on this prefix +# allowlist is treated as potentially side-effecting and gated behind +# the mutation opt-in. Keep this list conservative — it's far better +# to over-block and force an explicit allow_mutations=True than to +# silently let the model run a delete. +_READ_ONLY_PREFIXES: tuple[str, ...] = ( + "list_", + "get_", + "head_", + "summarize_", + "preview_", + "validate_", + "test_", + "search_", + "describe_", + "fetch_", + "compute_", +) + +_MUTATION_OPT_IN_ENV = "LOCUS_USE_OCI_ALLOW_MUTATIONS" + + +class _UseOCIError(RuntimeError): + """Raised for any user-facing failure inside ``use_oci``. + + Surfaces as the tool's error string so the model gets a readable + message instead of a traceback. + """ + + +def _is_read_only(operation: str) -> bool: + return any(operation.startswith(p) for p in _READ_ONLY_PREFIXES) + + +def _mutations_enabled(explicit: bool) -> bool: + if explicit: + return True + return os.environ.get(_MUTATION_OPT_IN_ENV, "").strip().lower() in {"1", "true", "yes"} + + +def _load_oci() -> Any: + try: + return importlib.import_module("oci") + except ImportError as exc: + raise _UseOCIError( + "The 'oci' SDK is not installed. Install with: " + "pip install 'locus-sdk[oci]' (or pip install oci)." + ) from exc + + +def _resolve_client_class(oci_mod: Any, service: str, client: str) -> type: + try: + service_mod = importlib.import_module(f"oci.{service}") + except ImportError as exc: + # Help the model self-correct: list a few service names so it + # can re-emit the call with a valid one. + import pkgutil # noqa: PLC0415 + + services = sorted( + m.name for m in pkgutil.iter_modules(oci_mod.__path__) if not m.name.startswith("_") + ) + raise _UseOCIError( + f"Unknown OCI service '{service}'. The OCI Python SDK has " + f"{len(services)} service modules under oci.* — examples: " + f"{', '.join(services[:15])}, … " + f"Pass the snake_case module name (e.g. 'identity', 'core', " + f"'database', 'object_storage')." + ) from exc + + client_cls = getattr(service_mod, client, None) + if client_cls is None or not isinstance(client_cls, type): + # List candidate clients in this service to nudge the model. + candidates = sorted( + name + for name in dir(service_mod) + if name.endswith("Client") and not name.startswith("_") + ) + raise _UseOCIError( + f"OCI service 'oci.{service}' has no client class '{client}'. " + f"Available clients: {', '.join(candidates) or '(none)'}." + ) + # mypy 3.12 needs the cast to narrow Any → type (no-any-return); + # mypy 3.14 sees the cast as redundant after the isinstance check. + # Suppressing both codes keeps both versions green. + return cast("type", client_cls) # type: ignore[unused-ignore, redundant-cast] + + +def _build_config_and_signer( + oci_mod: Any, + profile: str, + auth_type: str, + region: str | None, +) -> tuple[dict[str, Any], Any]: + """Return ``(config, signer)`` suitable for an OCI client constructor. + + For ``api_key`` auth the SDK reads everything from the config dict; + ``signer`` is ``None``. For ``security_token`` (delegation token + from ``oci session authenticate``) we build a ``SecurityTokenSigner`` + over the private key referenced by ``key_file`` in the profile. + """ + auth_type = (auth_type or "api_key").lower() + + if auth_type == "instance_principal": + signer = oci_mod.auth.signers.InstancePrincipalsSecurityTokenSigner() + config: dict[str, Any] = {"region": region or signer.region} + return config, signer + + if auth_type == "resource_principal": + signer = oci_mod.auth.signers.get_resource_principals_signer() + config = {"region": region or signer.region} + return config, signer + + # File-based profiles (api_key or security_token) + try: + config = oci_mod.config.from_file(profile_name=profile) + except oci_mod.exceptions.ConfigFileNotFound as exc: + raise _UseOCIError( + f"OCI config file not found. Looked for profile '{profile}' in ~/.oci/config." + ) from exc + except oci_mod.exceptions.ProfileNotFound as exc: + raise _UseOCIError(f"Profile '{profile}' is not present in ~/.oci/config.") from exc + + if region: + config["region"] = region + + if auth_type == "security_token": + token_file = config.get("security_token_file") + if not token_file: + raise _UseOCIError( + f"Profile '{profile}' has no 'security_token_file' — " + f"either run `oci session authenticate --profile-name " + f"{profile}` first, or use auth_type='api_key'." + ) + token = Path(token_file).expanduser().read_text(encoding="utf-8").strip() + private_key = oci_mod.signer.load_private_key_from_file(config["key_file"]) + signer = oci_mod.auth.signers.SecurityTokenSigner(token, private_key) + return config, signer + + if auth_type != "api_key": + raise _UseOCIError( + f"Unsupported auth_type '{auth_type}'. Use one of: " + f"api_key, security_token, instance_principal, resource_principal." + ) + return config, None + + +def _serialize(value: Any) -> Any: + """Recursively turn an OCI SDK model object into JSON-safe data. + + OCI SDK response models expose an ``attribute_map`` (snake_case + Python attr → camelCase wire name); we walk it to mirror the + REST response shape. + """ + if value is None or isinstance(value, (bool, int, float, str)): + return value + if isinstance(value, Decimal): + return float(value) + if isinstance(value, (_dt.date, _dt.datetime)): + return value.isoformat() + if isinstance(value, Enum): + return value.value + if isinstance(value, (list, tuple, set)): + return [_serialize(v) for v in value] + if isinstance(value, dict): + return {str(k): _serialize(v) for k, v in value.items()} + attr_map = getattr(value, "attribute_map", None) + if isinstance(attr_map, dict): + return { + wire_name: _serialize(getattr(value, py_attr, None)) + for py_attr, wire_name in attr_map.items() + } + return str(value) + + +@tool(idempotent=True) +def use_oci( + service: str, + client: str, + operation: str, + parameters: dict[str, Any] | None = None, + compartment_id: str | None = None, + region: str | None = None, + profile: str = "DEFAULT", + auth_type: str = "api_key", + label: str = "", + allow_mutations: bool = False, +) -> dict[str, Any]: + """Call any operation on any OCI service via the OCI Python SDK. + + This is a single open-specification tool that dispatches to the + full OCI control plane. Pick the service module, the client class + inside it, the operation (SDK method name), and the parameters — + the tool builds the right client with auth, invokes the method, + and returns the serialized response. + + Read-only by default. Mutating operations (anything that isn't a + list/get/head/summarize/preview/validate/test/search/describe/fetch/ + compute_) are refused unless ``allow_mutations=True`` is passed + explicitly, or the ``LOCUS_USE_OCI_ALLOW_MUTATIONS=1`` env var + is set. This is intentional — the model will sometimes pick a + mutating verb when a read would do. + + Args: + service: OCI service module under ``oci.*`` in snake_case. + Examples: ``"identity"``, ``"core"``, ``"database"``, + ``"object_storage"``, ``"vault"``, ``"bastion"``, + ``"generative_ai"``, ``"functions"``, ``"resource_search"``. + The OCI Python SDK has ~190 such modules. + client: Client class inside that module (CamelCase, ends in + ``Client``). Most services have one — e.g. + ``"IdentityClient"``, ``"DatabaseClient"``, + ``"ObjectStorageClient"``. ``core`` has three: + ``ComputeClient``, ``BlockstorageClient``, + ``VirtualNetworkClient``. + operation: SDK method name in snake_case — mirrors the REST + operation id. Examples: ``"list_compartments"``, + ``"get_autonomous_database"``, ``"list_buckets"``, + ``"list_instances"``. + parameters: Keyword arguments to pass to the operation. For + list/get calls this is typically ``{"compartment_id": "..."}`` + plus optional filters like ``{"lifecycle_state": "AVAILABLE"}``. + For most ``get_*`` calls it's the resource OCID, e.g. + ``{"autonomous_database_id": "ocid1.autonomousdatabase..."}``. + compartment_id: Convenience — if the operation accepts + ``compartment_id`` and you didn't put it in ``parameters``, + it's merged in automatically. Pass the tenancy OCID for + tenancy-scoped listings, or a specific compartment OCID. + region: OCI region (e.g. ``"us-ashburn-1"``, ``"us-chicago-1"``, + ``"ca-toronto-1"``). Falls back to the profile's home region. + profile: ``~/.oci/config`` profile name. Defaults to ``DEFAULT``. + auth_type: One of ``api_key`` (default, permanent key), + ``security_token`` (session token from + ``oci session authenticate``), ``instance_principal`` + (running on an OCI compute instance with a dynamic group), + or ``resource_principal`` (running as an OCI Function or + other resource principal). + label: Free-text hint describing the user-facing intent of the + call. Surfaces in logs and traces — not sent to OCI. + allow_mutations: Explicit opt-in to run a write operation + (anything that isn't ``list_/get_/head_/summarize_/...``). + Without this AND without the env var, write operations + return an error instead of executing. + + Returns: + A dict with ``status`` (``"ok"`` or ``"error"``), ``operation``, + ``service``, ``client``, ``region``, and either ``data`` (the + serialized response) or ``error`` (a human-readable message). + + Examples: + List every compartment in a tenancy:: + + use_oci( + service="identity", + client="IdentityClient", + operation="list_compartments", + parameters={ + "compartment_id": "ocid1.tenancy.oc1..xxx", + "compartment_id_in_subtree": True, + }, + profile="DEFAULT", + ) + + Get one Autonomous Database:: + + use_oci( + service="database", + client="DatabaseClient", + operation="get_autonomous_database", + parameters={"autonomous_database_id": "ocid1.autonomousdatabase..."}, + region="ca-toronto-1", + ) + + Free-form search across all resources in a compartment:: + + use_oci( + service="resource_search", + client="ResourceSearchClient", + operation="search_resources", + parameters={ + "search_details": { + "type": "Structured", + "query": "query all resources where compartmentId = '...'", + } + }, + ) + """ + parameters = dict(parameters or {}) + if compartment_id and "compartment_id" not in parameters: + parameters["compartment_id"] = compartment_id + + if not _is_read_only(operation) and not _mutations_enabled(allow_mutations): + return { + "status": "error", + "service": service, + "client": client, + "operation": operation, + "error": ( + f"Operation '{operation}' looks mutating (it doesn't start " + f"with a read-only prefix like list_/get_/head_/summarize_/" + f"describe_). Refusing by default. Pass allow_mutations=True " + f"or set {_MUTATION_OPT_IN_ENV}=1 to override." + ), + } + + try: + oci_mod = _load_oci() + client_cls = _resolve_client_class(oci_mod, service, client) + config, signer = _build_config_and_signer(oci_mod, profile, auth_type, region) + + client_kwargs: dict[str, Any] = {"config": config} + if signer is not None: + client_kwargs["signer"] = signer + instance = client_cls(**client_kwargs) + + method = getattr(instance, operation, None) + if method is None or not callable(method): + available = sorted( + name + for name in dir(instance) + if not name.startswith("_") and callable(getattr(instance, name, None)) + ) + close = [a for a in available if operation.split("_", 1)[0] in a][:15] + raise _UseOCIError( + f"Client '{client}' has no operation '{operation}'. " + f"Did you mean one of: {', '.join(close) or available[:15]}?" + ) + + response = method(**parameters) + data = _serialize(getattr(response, "data", response)) + status_code = getattr(response, "status", None) + request_id = ( + response.headers.get("opc-request-id") + if hasattr(response, "headers") and response.headers + else None + ) + + result: dict[str, Any] = { + "status": "ok", + "service": service, + "client": client, + "operation": operation, + "region": config.get("region"), + "data": data, + } + if status_code is not None: + result["http_status"] = status_code + if request_id: + result["opc_request_id"] = request_id + if label: + result["label"] = label + return result + + except _UseOCIError as exc: + return { + "status": "error", + "service": service, + "client": client, + "operation": operation, + "error": str(exc), + } + except Exception as exc: # noqa: BLE001 — bubble OCI ServiceError text up + # OCI's ServiceError prints a structured one-liner with the + # REST status, opc-request-id, and message — better to surface + # that verbatim than to swallow it. + return { + "status": "error", + "service": service, + "client": client, + "operation": operation, + "error": f"{type(exc).__name__}: {exc}", + "parameters_sent": _serialize(parameters), + } + + +def _json_str(obj: Any) -> str: + """Test helper — invoke ``use_oci`` and return its result as JSON.""" + return json.dumps(obj, indent=2, default=str) + + +# --------------------------------------------------------------------------- +# describe_oci — runtime introspection of the OCI Python SDK +# --------------------------------------------------------------------------- + +# OCI docstrings use Sphinx-style param lines: +# :param str compartment_id: (required) +# The OCID of the compartment... +# :param int limit: (optional) +# Maximum items... +# Type is between ``:param `` and the next space; the ``(required)`` or +# ``(optional)`` marker (when present) is at the start of the description. +_PARAM_RE = re.compile( + r":param\s+([^\s:]+)\s+([A-Za-z_][A-Za-z0-9_]*):\s*(.*?)(?=\n:|\Z)", re.DOTALL +) + + +def _parse_operation_doc(doc: str) -> list[dict[str, Any]]: + """Pull a structured parameter list out of an OCI SDK method docstring.""" + params: list[dict[str, Any]] = [] + for match in _PARAM_RE.finditer(doc or ""): + type_str, name, description = match.group(1), match.group(2), match.group(3) + description = inspect.cleandoc(description).strip() + required = description.lower().startswith("(required)") + # Strip the leading (required)/(optional) marker for cleanliness. + description = re.sub(r"^\((required|optional)\)\s*", "", description, flags=re.IGNORECASE) + params.append( + { + "name": name, + "type": type_str, + "required": required, + "description": description[:400], # truncate verbose descriptions + } + ) + return params + + +def _list_service_modules(oci_mod: Any) -> list[str]: + import pkgutil # noqa: PLC0415 + + return sorted( + m.name for m in pkgutil.iter_modules(oci_mod.__path__) if not m.name.startswith("_") + ) + + +def _list_clients_in_service(service_mod: Any) -> list[str]: + return sorted( + name + for name in dir(service_mod) + if name.endswith("Client") + and not name.startswith("_") + and inspect.isclass(getattr(service_mod, name, None)) + ) + + +def _list_operations_on_client(client_cls: type) -> list[str]: + return sorted( + name + for name, member in inspect.getmembers(client_cls, predicate=inspect.isfunction) + if not name.startswith("_") + ) + + +@tool(idempotent=True) +def describe_oci( + service: str | None = None, + client: str | None = None, + operation: str | None = None, +) -> dict[str, Any]: + """Introspect the OCI Python SDK — discover services, clients, operations, params. + + Use this *before* ``use_oci`` whenever you're unsure what shape the + call needs to take. It progressively zooms in: + + - No args: list every service module under ``oci.*`` (~190 of them). + - ``service`` only: list every ``*Client`` class inside that service. + - ``service`` + ``client``: list every operation (method) on that + client, separated into read-only and mutating. + - ``service`` + ``client`` + ``operation``: return the operation's + parameter schema — name, type, required/optional, description — + parsed from the OCI SDK docstring, plus a snippet of the human + description and whether the operation is read-only. + + Args: + service: Snake-case service module name (e.g. ``"identity"``, + ``"core"``, ``"database"``, ``"object_storage"``). + client: CamelCase client class name (e.g. ``"IdentityClient"``, + ``"ComputeClient"``). + operation: Snake-case operation name (e.g. ``"list_compartments"``). + + Returns: + A dict whose shape depends on how deep you drilled. Always + includes ``status``: ``"ok"`` or ``"error"``. + + Examples: + Discover all services:: + + describe_oci() + # {"status": "ok", "level": "services", "services": ["access_governance_cp", ...]} + + Find clients in a service:: + + describe_oci(service="core") + # {"status": "ok", "level": "clients", + # "clients": ["BlockstorageClient", "ComputeClient", "VirtualNetworkClient"]} + + See operation parameter schema:: + + describe_oci( + service="identity", + client="IdentityClient", + operation="list_compartments", + ) + # {"status": "ok", "level": "operation", + # "read_only": True, + # "parameters": [ + # {"name": "compartment_id", "type": "str", "required": True, ...}, + # {"name": "compartment_id_in_subtree", "type": "bool", ...}, + # ... + # ]} + """ + try: + oci_mod = _load_oci() + except _UseOCIError as exc: + return {"status": "error", "error": str(exc)} + + if service is None: + services = _list_service_modules(oci_mod) + return { + "status": "ok", + "level": "services", + "count": len(services), + "services": services, + } + + try: + service_mod = importlib.import_module(f"oci.{service}") + except ImportError: + services = _list_service_modules(oci_mod) + return { + "status": "error", + "error": f"No OCI service module named '{service}'.", + "available_services_sample": services[:30], + "total_services": len(services), + } + + if client is None: + clients = _list_clients_in_service(service_mod) + return { + "status": "ok", + "level": "clients", + "service": service, + "clients": clients, + } + + client_cls = getattr(service_mod, client, None) + if client_cls is None or not inspect.isclass(client_cls): + return { + "status": "error", + "error": f"OCI service 'oci.{service}' has no client class '{client}'.", + "available_clients": _list_clients_in_service(service_mod), + } + + if operation is None: + ops = _list_operations_on_client(client_cls) + read = [o for o in ops if _is_read_only(o)] + write = [o for o in ops if not _is_read_only(o)] + return { + "status": "ok", + "level": "operations", + "service": service, + "client": client, + "count": len(ops), + "read_only_operations": read, + "mutating_operations": write, + } + + method = getattr(client_cls, operation, None) + if method is None or not callable(method): + ops = _list_operations_on_client(client_cls) + close = [o for o in ops if operation.split("_", 1)[0] in o][:20] + return { + "status": "error", + "error": f"Client '{client}' has no operation '{operation}'.", + "suggestions": close, + } + + doc = inspect.getdoc(method) or "" + summary = doc.split("\n\n", 1)[0].strip() if doc else "" + parameters = _parse_operation_doc(doc) + try: + sig = str(inspect.signature(method)) + except (TypeError, ValueError): + sig = "" + + return { + "status": "ok", + "level": "operation", + "service": service, + "client": client, + "operation": operation, + "signature": f"{operation}{sig}" if sig else operation, + "read_only": _is_read_only(operation), + "summary": summary[:600], + "parameters": parameters, + "required_parameters": [p["name"] for p in parameters if p["required"]], + } + + +__all__ = ["describe_oci", "use_oci"] diff --git a/tests/unit/test_tools_oci.py b/tests/unit/test_tools_oci.py new file mode 100644 index 00000000..44653bd2 --- /dev/null +++ b/tests/unit/test_tools_oci.py @@ -0,0 +1,683 @@ +# Copyright (c) 2025, 2026 Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v1.0 as shown at +# https://oss.oracle.com/licenses/upl/ + +"""Unit tests for the open-spec OCI tools (`use_oci` + `describe_oci`). + +These cover the pure-Python logic — docstring parsing, mutation gating, +SDK introspection, signer routing, response serialisation, error +handling — without hitting any real OCI endpoint. The integration-y +parts (live calls, JSON payloads from the wire) are exercised by the +notebook 70 walk-through against a real tenancy. +""" + +from __future__ import annotations + +import asyncio +import json +from datetime import UTC, datetime +from decimal import Decimal +from enum import Enum +from unittest.mock import MagicMock, patch + +import pytest + +from locus.tools import describe_oci, use_oci +from locus.tools.oci import ( + _MUTATION_OPT_IN_ENV, + _is_read_only, + _mutations_enabled, + _parse_operation_doc, + _serialize, + _UseOCIError, +) + + +# --------------------------------------------------------------------------- +# Pure helpers +# --------------------------------------------------------------------------- + + +class TestReadOnlyClassifier: + """`_is_read_only` decides which operations the mutation gate lets through.""" + + @pytest.mark.parametrize( + "name", + [ + "list_compartments", + "get_instance", + "head_object", + "summarize_metrics", + "describe_workspace", + "search_resources", + "fetch_secret_bundle", + "compute_pricing", + "preview_change", + "validate_workflow", + "test_connection", + ], + ) + def test_read_only_prefixes(self, name: str) -> None: + assert _is_read_only(name) + + @pytest.mark.parametrize( + "name", + [ + "create_compartment", + "update_instance", + "delete_bucket", + "change_compartment", + "terminate_instance", + "start_workspace", + "stop_workspace", + "rotate_key", + "attach_volume", + "post_message", + ], + ) + def test_mutating_prefixes(self, name: str) -> None: + assert not _is_read_only(name) + + +class TestMutationGate: + """The mutation gate is opt-in by argument OR by env var.""" + + def test_default_blocks_mutations(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(_MUTATION_OPT_IN_ENV, raising=False) + assert _mutations_enabled(False) is False + + def test_explicit_kwarg_overrides(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(_MUTATION_OPT_IN_ENV, raising=False) + assert _mutations_enabled(True) is True + + @pytest.mark.parametrize("value", ["1", "true", "yes", "TRUE", "YES"]) + def test_env_truthy_values(self, monkeypatch: pytest.MonkeyPatch, value: str) -> None: + monkeypatch.setenv(_MUTATION_OPT_IN_ENV, value) + assert _mutations_enabled(False) is True + + @pytest.mark.parametrize("value", ["0", "false", "no", ""]) + def test_env_falsy_values(self, monkeypatch: pytest.MonkeyPatch, value: str) -> None: + monkeypatch.setenv(_MUTATION_OPT_IN_ENV, value) + assert _mutations_enabled(False) is False + + +class TestDocstringParser: + """`_parse_operation_doc` lifts :param TYPE name: blocks out of OCI SDK docstrings.""" + + def test_required_and_optional(self) -> None: + doc = ( + "Lists the compartments.\n\n" + ":param str compartment_id: (required)\n" + " The OCID of the compartment.\n" + ":param int limit: (optional)\n" + " Max items to return.\n" + ":param bool compartment_id_in_subtree: (optional)\n" + " Whether to recurse.\n" + ) + params = _parse_operation_doc(doc) + names = [p["name"] for p in params] + assert names == ["compartment_id", "limit", "compartment_id_in_subtree"] + + cid = params[0] + assert cid["type"] == "str" + assert cid["required"] is True + assert cid["description"].startswith("The OCID") + assert "(required)" not in cid["description"] + + assert params[1]["type"] == "int" + assert params[1]["required"] is False + assert params[2]["type"] == "bool" + + def test_no_params(self) -> None: + assert _parse_operation_doc("") == [] + assert _parse_operation_doc("Plain docstring, no :param: blocks.") == [] + + def test_truncates_long_description(self) -> None: + long = "x " * 1000 + doc = f":param str foo: (required)\n {long}" + params = _parse_operation_doc(doc) + assert len(params[0]["description"]) <= 400 + + +class TestSerializer: + """`_serialize` turns OCI SDK model objects into JSON-safe Python.""" + + def test_primitives(self) -> None: + for v in [None, True, 42, 3.14, "hi"]: + assert _serialize(v) == v + + def test_decimal(self) -> None: + assert _serialize(Decimal("1.5")) == 1.5 + + def test_datetime(self) -> None: + dt = datetime(2026, 5, 22, 12, 30, 0, tzinfo=UTC) + assert _serialize(dt) == "2026-05-22T12:30:00+00:00" + + def test_enum(self) -> None: + class State(Enum): + ACTIVE = "ACTIVE" + + assert _serialize(State.ACTIVE) == "ACTIVE" + + def test_list_and_tuple_and_set(self) -> None: + assert _serialize([1, 2, 3]) == [1, 2, 3] + assert _serialize((1, 2, 3)) == [1, 2, 3] + assert sorted(_serialize({1, 2, 3})) == [1, 2, 3] + + def test_dict(self) -> None: + assert _serialize({"a": 1, "b": [2, 3]}) == {"a": 1, "b": [2, 3]} + + def test_oci_model_via_attribute_map(self) -> None: + """OCI SDK models expose `attribute_map` (snake_case -> wire camelCase).""" + m = MagicMock() + m.attribute_map = {"compartment_id": "compartmentId", "name": "name"} + m.compartment_id = "ocid1.tenancy.oc1..x" + m.name = "test" + out = _serialize(m) + assert out == {"compartmentId": "ocid1.tenancy.oc1..x", "name": "test"} + + def test_nested_model_inside_list(self) -> None: + inner = MagicMock() + inner.attribute_map = {"id": "id"} + inner.id = "ocid1.compartment.oc1..y" + out = _serialize([inner, inner]) + assert out == [{"id": "ocid1.compartment.oc1..y"}, {"id": "ocid1.compartment.oc1..y"}] + + def test_unknown_type_falls_back_to_str(self) -> None: + class Weird: + def __str__(self) -> str: + return "" + + assert _serialize(Weird()) == "" + + +# --------------------------------------------------------------------------- +# use_oci — high-level behaviour +# --------------------------------------------------------------------------- + + +def _run(coro): + return asyncio.get_event_loop().run_until_complete(coro) + + +class TestUseOCISchema: + def test_schema_required_keys(self) -> None: + params = use_oci.parameters + assert params["required"] == ["service", "client", "operation"] + for k in ( + "service", + "client", + "operation", + "parameters", + "compartment_id", + "region", + "profile", + "auth_type", + "label", + "allow_mutations", + ): + assert k in params["properties"] + + def test_is_idempotent(self) -> None: + # idempotent=True is set on the decorator so a model re-emitting + # the same (service, client, operation, parameters) gets the + # previous result rather than a fresh call. + assert use_oci.idempotent is True + + +class TestUseOCIMutationGate: + """The gate is the first thing use_oci does — covered without any OCI auth.""" + + @pytest.mark.asyncio + async def test_mutating_operation_refused_by_default( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(_MUTATION_OPT_IN_ENV, raising=False) + out = json.loads( + await use_oci.execute( + service="object_storage", + client="ObjectStorageClient", + operation="delete_bucket", + parameters={"namespace_name": "x", "bucket_name": "y"}, + profile="DOES_NOT_MATTER", + ) + ) + assert out["status"] == "error" + assert "delete_bucket" in out["error"] + assert "mutating" in out["error"].lower() + + @pytest.mark.asyncio + async def test_allow_mutations_kwarg_unblocks(self, monkeypatch: pytest.MonkeyPatch) -> None: + """With allow_mutations=True we proceed past the gate (and fail later at auth).""" + monkeypatch.delenv(_MUTATION_OPT_IN_ENV, raising=False) + out = json.loads( + await use_oci.execute( + service="object_storage", + client="ObjectStorageClient", + operation="delete_bucket", + parameters={"namespace_name": "x", "bucket_name": "y"}, + profile="DOES_NOT_EXIST_INTENTIONALLY", + allow_mutations=True, + ) + ) + assert out["status"] == "error" + # The error should be the auth-side failure, NOT the mutation refusal. + assert "mutating" not in out["error"].lower() + + @pytest.mark.asyncio + async def test_env_var_unblocks(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(_MUTATION_OPT_IN_ENV, "1") + out = json.loads( + await use_oci.execute( + service="object_storage", + client="ObjectStorageClient", + operation="delete_bucket", + parameters={"namespace_name": "x", "bucket_name": "y"}, + profile="DOES_NOT_EXIST_INTENTIONALLY", + ) + ) + assert out["status"] == "error" + assert "mutating" not in out["error"].lower() + + +class TestUseOCICompartmentIdMerge: + """compartment_id is a convenience param; it's auto-merged into parameters.""" + + @pytest.mark.asyncio + async def test_compartment_id_merged_when_absent(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Confirm the merge happens before auth resolution fires.""" + captured: dict[str, object] = {} + + # Patch the auth resolver so we can inspect what parameters reached it. + from locus.tools import oci as oci_module + + original = oci_module._build_config_and_signer + + def spy(*args, **kwargs): # noqa: ANN001, ANN002 + raise _UseOCIError("intentional — captured for inspection") + + monkeypatch.setattr(oci_module, "_build_config_and_signer", spy) + + out = json.loads( + await use_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + compartment_id="ocid1.tenancy.oc1..xxx", + profile="X", + ) + ) + assert out["status"] == "error" + assert "intentional" in out["error"] + + # Restore (pytest does this anyway via monkeypatch, but explicit). + monkeypatch.setattr(oci_module, "_build_config_and_signer", original) + + @pytest.mark.asyncio + async def test_explicit_compartment_in_parameters_wins(self) -> None: + """If the model passes compartment_id in parameters, the kwarg is not overwritten.""" + # We can't easily inspect what landed in `method(**parameters)` without + # patching the SDK; the merge logic is straightforward (`if not in parameters`). + # This test asserts no exception path is triggered when both are present. + out = json.loads( + await use_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + parameters={"compartment_id": "ocid1.tenancy.oc1..A"}, + compartment_id="ocid1.tenancy.oc1..B", # should be ignored + profile="DOES_NOT_EXIST_INTENTIONALLY", + ) + ) + assert out["status"] == "error" + # Whatever the error is, it shouldn't be the mutation gate. + assert "mutating" not in out["error"].lower() + + +class TestUseOCIErrorPaths: + """Service / client / operation resolution errors get surfaced cleanly.""" + + @pytest.mark.asyncio + async def test_unknown_service(self) -> None: + out = json.loads( + await use_oci.execute( + service="not_a_real_service", + client="X", + operation="list_things", + profile="DOES_NOT_EXIST_INTENTIONALLY", + ) + ) + assert out["status"] == "error" + assert "not_a_real_service" in out["error"] + + @pytest.mark.asyncio + async def test_unknown_client_in_real_service(self) -> None: + out = json.loads( + await use_oci.execute( + service="identity", + client="NotAClient", + operation="list_things", + profile="DOES_NOT_EXIST_INTENTIONALLY", + ) + ) + assert out["status"] == "error" + assert "NotAClient" in out["error"] + + +# --------------------------------------------------------------------------- +# describe_oci +# --------------------------------------------------------------------------- + + +class TestDescribeOCILevels: + """describe_oci walks the SDK at four levels — all four work without OCI auth.""" + + @pytest.mark.asyncio + async def test_level_1_services(self) -> None: + r = json.loads(await describe_oci.execute()) + assert r["status"] == "ok" + assert r["level"] == "services" + assert r["count"] > 100 + assert "identity" in r["services"] + assert "core" in r["services"] + + @pytest.mark.asyncio + async def test_level_2_clients(self) -> None: + r = json.loads(await describe_oci.execute(service="core")) + assert r["status"] == "ok" + assert r["level"] == "clients" + assert "ComputeClient" in r["clients"] + assert "VirtualNetworkClient" in r["clients"] + + @pytest.mark.asyncio + async def test_level_3_operations_partitioned(self) -> None: + r = json.loads( + await describe_oci.execute(service="object_storage", client="ObjectStorageClient") + ) + assert r["status"] == "ok" + assert r["level"] == "operations" + assert "get_namespace" in r["read_only_operations"] + # mutating bucket lifecycle operations + assert any("delete_" in o for o in r["mutating_operations"]) + assert any("create_" in o for o in r["mutating_operations"]) + + @pytest.mark.asyncio + async def test_level_4_operation_schema(self) -> None: + r = json.loads( + await describe_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + ) + ) + assert r["status"] == "ok" + assert r["level"] == "operation" + assert r["read_only"] is True + assert "compartment_id" in r["required_parameters"] + # All parameter entries have the structured shape we promise. + for p in r["parameters"]: + assert set(p.keys()) >= {"name", "type", "required", "description"} + + +class TestDescribeOCIErrors: + @pytest.mark.asyncio + async def test_unknown_service_returns_suggestions(self) -> None: + r = json.loads(await describe_oci.execute(service="not_a_real_service")) + assert r["status"] == "error" + assert "not_a_real_service" in r["error"] + assert isinstance(r.get("available_services_sample"), list) + assert r["total_services"] > 100 + + @pytest.mark.asyncio + async def test_unknown_client_returns_candidates(self) -> None: + r = json.loads(await describe_oci.execute(service="identity", client="NotAClient")) + assert r["status"] == "error" + assert "NotAClient" in r["error"] + assert "IdentityClient" in r.get("available_clients", []) + + @pytest.mark.asyncio + async def test_unknown_operation_returns_suggestions(self) -> None: + r = json.loads( + await describe_oci.execute( + service="identity", + client="IdentityClient", + operation="list_widgets_that_dont_exist", + ) + ) + assert r["status"] == "error" + assert "list_widgets_that_dont_exist" in r["error"] + + +# --------------------------------------------------------------------------- +# Auth router +# --------------------------------------------------------------------------- + + +class TestAuthRouter: + """`_build_config_and_signer` picks the right auth path per auth_type.""" + + def test_unsupported_auth_type_raises(self) -> None: + """The 'Unsupported auth_type' check fires after a successful profile load.""" + from locus.tools import oci as oci_module + + oci_mod = oci_module._load_oci() + with ( + patch.object(oci_mod.config, "from_file", return_value={"region": "us-ashburn-1"}), + pytest.raises(_UseOCIError, match="Unsupported auth_type"), + ): + oci_module._build_config_and_signer(oci_mod, profile="X", auth_type="nope", region=None) + + def test_missing_profile_raises(self) -> None: + from locus.tools import oci as oci_module + + with pytest.raises(_UseOCIError): + oci_module._build_config_and_signer( + oci_module._load_oci(), + profile="DEFINITELY_NOT_A_REAL_PROFILE_XYZ123", + auth_type="api_key", + region=None, + ) + + def test_instance_principal_signer_is_built(self) -> None: + """We patch the OCI SDK's signer constructor so this stays offline.""" + from locus.tools import oci as oci_module + + with patch.object( + oci_module._load_oci().auth.signers, + "InstancePrincipalsSecurityTokenSigner", + ) as ctor: + ctor.return_value.region = "us-ashburn-1" + cfg, signer = oci_module._build_config_and_signer( + oci_module._load_oci(), + profile="ignored", + auth_type="instance_principal", + region=None, + ) + assert cfg["region"] == "us-ashburn-1" + assert signer is ctor.return_value + + def test_resource_principal_signer_is_built(self) -> None: + """`resource_principal` path goes through ``get_resource_principals_signer``.""" + from locus.tools import oci as oci_module + + with patch.object( + oci_module._load_oci().auth.signers, + "get_resource_principals_signer", + ) as factory: + factory.return_value.region = "us-phoenix-1" + cfg, signer = oci_module._build_config_and_signer( + oci_module._load_oci(), + profile="ignored", + auth_type="resource_principal", + region=None, + ) + assert cfg["region"] == "us-phoenix-1" + assert signer is factory.return_value + + def test_security_token_missing_token_file_raises(self) -> None: + from locus.tools import oci as oci_module + + oci_mod = oci_module._load_oci() + with ( + patch.object(oci_mod.config, "from_file", return_value={"region": "us-ashburn-1"}), + pytest.raises(_UseOCIError, match="security_token_file"), + ): + oci_module._build_config_and_signer( + oci_mod, profile="X", auth_type="security_token", region=None + ) + + def test_api_key_returns_config_with_no_signer(self) -> None: + from locus.tools import oci as oci_module + + oci_mod = oci_module._load_oci() + fake_cfg = {"region": "us-ashburn-1", "user": "x"} + with patch.object(oci_mod.config, "from_file", return_value=fake_cfg): + cfg, signer = oci_module._build_config_and_signer( + oci_mod, profile="X", auth_type="api_key", region=None + ) + assert cfg is fake_cfg + assert signer is None + + def test_region_override(self) -> None: + """Passing ``region=`` overrides the profile's region.""" + from locus.tools import oci as oci_module + + oci_mod = oci_module._load_oci() + with patch.object(oci_mod.config, "from_file", return_value={"region": "us-ashburn-1"}): + cfg, _ = oci_module._build_config_and_signer( + oci_mod, profile="X", auth_type="api_key", region="us-chicago-1" + ) + assert cfg["region"] == "us-chicago-1" + + +# --------------------------------------------------------------------------- +# Full success path — mock the client + method so we exercise the response +# serialisation and the http_status/opc_request_id propagation. +# --------------------------------------------------------------------------- + + +class TestUseOCISuccessPath: + """Patch the SDK call to confirm response packaging works end-to-end.""" + + @pytest.mark.asyncio + async def test_successful_call_returns_serialised_data( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + from locus.tools import oci as oci_module + + # Mock the auth resolver to skip ~/.oci/config entirely. + monkeypatch.setattr( + oci_module, + "_build_config_and_signer", + lambda *a, **kw: ({"region": "us-ashburn-1"}, None), + ) + + # Mock the client class so its constructor returns a stub with + # list_compartments() -> Response. + fake_response = MagicMock() + fake_response.status = 200 + fake_response.headers = {"opc-request-id": "REQ-ABCDEF123"} + # Build a fake OCI model object with attribute_map for serialisation + item = MagicMock() + item.attribute_map = {"id": "id", "name": "name"} + item.id = "ocid1.compartment.oc1..xxx" + item.name = "test-compartment" + fake_response.data = [item] + + fake_client = MagicMock() + fake_client.list_compartments = MagicMock(return_value=fake_response) + + fake_client_cls = MagicMock(return_value=fake_client) + monkeypatch.setattr(oci_module, "_resolve_client_class", lambda *a, **kw: fake_client_cls) + + out = json.loads( + await use_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + parameters={"compartment_id": "ocid1.tenancy.oc1..xxx"}, + profile="ignored", + region="us-ashburn-1", + label="get all compartments", + ) + ) + + assert out["status"] == "ok" + assert out["http_status"] == 200 + assert out["opc_request_id"] == "REQ-ABCDEF123" + assert out["region"] == "us-ashburn-1" + assert out["label"] == "get all compartments" + assert out["data"] == [{"id": "ocid1.compartment.oc1..xxx", "name": "test-compartment"}] + + @pytest.mark.asyncio + async def test_operation_not_on_client_gives_suggestions( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + from locus.tools import oci as oci_module + + monkeypatch.setattr( + oci_module, + "_build_config_and_signer", + lambda *a, **kw: ({"region": "us-ashburn-1"}, None), + ) + + # Build a client class that lacks the operation name, but has list_* + class FakeClient: + def __init__(self, **kw): # accept config=, signer= + pass + + def list_compartments(self, **kw): + return None + + def list_users(self, **kw): + return None + + monkeypatch.setattr(oci_module, "_resolve_client_class", lambda *a, **kw: FakeClient) + + out = json.loads( + await use_oci.execute( + service="identity", + client="IdentityClient", + operation="list_widgets", + profile="ignored", + ) + ) + assert out["status"] == "error" + assert "list_widgets" in out["error"] + + @pytest.mark.asyncio + async def test_oci_service_error_is_surfaced_with_parameters( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """If the SDK call raises, we wrap the exception text and echo parameters.""" + from locus.tools import oci as oci_module + + monkeypatch.setattr( + oci_module, + "_build_config_and_signer", + lambda *a, **kw: ({"region": "us-ashburn-1"}, None), + ) + + class BoomClient: + def __init__(self, **kw): + pass + + def list_compartments(self, **kw): + raise RuntimeError("ServiceError: 404 NotAuthorized") + + monkeypatch.setattr(oci_module, "_resolve_client_class", lambda *a, **kw: BoomClient) + + out = json.loads( + await use_oci.execute( + service="identity", + client="IdentityClient", + operation="list_compartments", + parameters={"compartment_id": "ocid1.tenancy.oc1..xxx"}, + profile="ignored", + ) + ) + assert out["status"] == "error" + assert "RuntimeError" in out["error"] + assert "NotAuthorized" in out["error"] + # The parameters we sent are echoed for debuggability + assert out["parameters_sent"]["compartment_id"] == "ocid1.tenancy.oc1..xxx"