diff --git a/README.md b/README.md index 49ba12e..ce67082 100644 --- a/README.md +++ b/README.md @@ -221,12 +221,6 @@ each tutorial is a working program against a real model. | **Production** | [`19_guardrails_security`](examples/tutorial_19_guardrails_security.py) · [`20_checkpoint_backends`](examples/tutorial_20_checkpoint_backends.py) · [`28_agent_server`](examples/tutorial_28_agent_server.py) · [`37_termination`](examples/tutorial_37_termination.py) | | **OCI** | [`29_model_providers`](examples/tutorial_29_model_providers.py) · [`40_oci_dac`](examples/tutorial_40_oci_dac.py) — Dedicated AI Cluster endpoints | -End-to-end demos: - -- [`examples/demos/po_approval/`](examples/demos/po_approval) — three agents (Procurement / Compliance / Approval Officer) debate a vendor PO against a live Oracle 26ai catalogue. Idempotent writes. Human consent gate. -- [`examples/demos/oracle_26ai/`](examples/demos/oracle_26ai) — full Oracle stack: OCI GenAI + Oracle 26ai vectors + skills + Reflexion + idempotent submit + checkpoints to OCI Object Storage. -- [`examples/demos/trip_team/`](examples/demos/trip_team) — same multi-agent shape on a Tokyo travel corpus. - ## Repo layout ```text diff --git a/docs/concepts/idempotency.md b/docs/concepts/idempotency.md index 136ab2b..4e19c90 100644 --- a/docs/concepts/idempotency.md +++ b/docs/concepts/idempotency.md @@ -52,5 +52,5 @@ call and the body runs. ## Source and tutorials - `src/locus/tools/decorator.py` — the `@tool` decorator and idempotency hook. -- Demo: [`examples/demos/po_approval/`](https://github.com/oracle-samples/locus/tree/main/examples/demos/po_approval) - shows idempotent `submit_po` and `email_cfo` deduping under retries. +- Tutorial: [`tutorial_03_tools_and_state.py`](https://github.com/oracle-samples/locus/blob/main/examples/tutorial_03_tools_and_state.py) + walks through `@tool(idempotent=True)` end-to-end. diff --git a/docs/how-to/quickstart.md b/docs/how-to/quickstart.md index 33aeb62..6bb8f70 100644 --- a/docs/how-to/quickstart.md +++ b/docs/how-to/quickstart.md @@ -198,9 +198,9 @@ anywhere FastAPI runs — see [Deploy](deploy.md). - **Read deeper.** [Agent Loop](../concepts/agent-loop.md) is the architectural reference for how all of this fits together. -- **Browse examples.** Thirty-seven progressive tutorials at - [`examples/`](https://github.com/oracle-samples/locus/tree/main/examples) - and three end-to-end demos under - [`examples/demos/`](https://github.com/oracle-samples/locus/tree/main/examples/demos). +- **Browse examples.** Forty progressive tutorials at + [`examples/`](https://github.com/oracle-samples/locus/tree/main/examples). + Each is a single runnable file that adds one idea on top of the + previous. - **Steer it.** [Hooks](../concepts/hooks.md) give you logging, telemetry, retry, guardrails, and steering as one-line additions. diff --git a/docs/index.md b/docs/index.md index 8506f11..e957f6a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -311,8 +311,9 @@ charges once. The agent stops. A three-agent vendor PO approval workflow against a live Oracle 26ai catalogue — Procurement and Compliance debate, hand off to an Approval -Officer, the human approves, idempotent writes fire — is in -[`examples/demos/po_approval/`](https://github.com/oracle-samples/locus/tree/main/examples/demos/po_approval). +Officer, the human approves, idempotent writes fire — runs end-to-end +in the multi-agent and idempotency tutorials under +[`examples/`](https://github.com/oracle-samples/locus/tree/main/examples). ## Introspect @@ -351,8 +352,8 @@ Read the [concepts](concepts/agent.md) for the *why*; read the ## Learn locus in an afternoon The [`examples/`](https://github.com/oracle-samples/locus/tree/main/examples) -tree is **40 tutorials** plus **3 end-to-end demos**. Every tutorial -is one runnable file and adds exactly one idea on top of the previous. +tree is **40 progressive tutorials**. Every tutorial is one runnable +file and adds exactly one idea on top of the previous. ### Track 1 — basics (first hour) @@ -414,14 +415,6 @@ The six in-process patterns plus A2A: [GSAR typed grounding](https://github.com/oracle-samples/locus/blob/main/examples/tutorial_39_gsar_typed_grounding.py) · [OCI Dedicated AI Cluster (DAC)](https://github.com/oracle-samples/locus/blob/main/examples/tutorial_40_oci_dac.py). -### End-to-end demos - -| Demo | What it shows | -|---|---| -| [`po_approval/`](https://github.com/oracle-samples/locus/tree/main/examples/demos/po_approval) | Three agents (Procurement / Compliance / Approval Officer) debate a vendor PO against a live Oracle 26ai catalogue. Idempotent writes. Human consent gate. | -| [`oracle_26ai/`](https://github.com/oracle-samples/locus/tree/main/examples/demos/oracle_26ai) | Full Oracle stack — OCI GenAI + Oracle 26ai vectors + skills + Reflexion + idempotent submit + checkpoints to OCI Object Storage. | -| [`trip_team/`](https://github.com/oracle-samples/locus/tree/main/examples/demos/trip_team) | Same multi-agent shape on a Tokyo travel corpus — three personas, one orchestrator, one durable thread. | - ## Then deploy When the agent is ready, ship it. `AgentServer` is a drop-in FastAPI diff --git a/examples/demos/README.md b/examples/demos/README.md deleted file mode 100644 index c4a69c4..0000000 --- a/examples/demos/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Demos - -Short visual walkthroughs of locus. - -## `build-an-agent.gif` - -![Build an agent in your editor, then run it.](build-an-agent.gif) - -What the recording shows, end-to-end: - -1. `bat` reveals a 50-line program — three tools (one of them - `@tool(idempotent=True)`) and an `Agent` against `oci:openai.gpt-5.5`. -2. `python agent.py` runs the program against OCI GenAI's V1 transport. -3. The output prints the model's reply, every tool that fired, and the - iteration count — that's the typed `RunResult` exposed by `run_sync`. - -The actual program is committed alongside as -[`agent_quickstart.py`](agent_quickstart.py). - -### Regenerating the GIF - -The recording was made with [VHS](https://github.com/charmbracelet/vhs) -and uses [`bat`](https://github.com/sharkdp/bat) for the syntax-highlighted -reveal: - -```bash -brew install vhs bat -cd examples/demos -export OCI_PROFILE= -vhs build-an-agent.tape -``` - -`build-an-agent.tape` is the source script — feel free to fork it for your own -walkthrough. diff --git a/examples/demos/agent_quickstart.py b/examples/demos/agent_quickstart.py deleted file mode 100644 index dc01d8b..0000000 --- a/examples/demos/agent_quickstart.py +++ /dev/null @@ -1,50 +0,0 @@ -# Locus — three tools, idempotent write, real ReAct loop. -# -# • @tool — Pydantic-validated, JSON schema auto-generated. -# • @tool(idempotent=True) — write-side dedup, no double-sends. -# • Agent(model="oci:...") — OCI GenAI V1, profile from OCI_PROFILE. - -from locus import Agent, tool - - -PAPERS = [ - ("Faiss: Efficient Similarity Search", 2017, 8400), - ("HNSW: Hierarchical Navigable Small World", 2018, 4500), - ("Pinecone whitepaper", 2022, 1200), -] - - -@tool -def search_papers(topic: str) -> list[dict]: - """Search the literature for a topic.""" - if any(k in topic.lower() for k in ("vector", "similarity", "ann")): - return [{"title": t, "year": y, "citations": c} for t, y, c in PAPERS] - return [] - - -@tool -def rank_by_citations(papers: list[dict]) -> dict: - """Pick the most-cited paper from the list.""" - return max(papers, key=lambda p: p["citations"]) - - -@tool(idempotent=True) -def email_report(to: str, subject: str, body: str) -> dict: - """Send the report. Idempotent — re-fires return cached results.""" - return {"status": "sent", "to": to, "chars": len(body)} - - -agent = Agent( - model="oci:openai.gpt-5.5", - tools=[search_papers, rank_by_citations, email_report], - system_prompt="Search → rank → email exactly once. One-sentence reply.", -) - -r = agent.run_sync( - "Find vector-DB papers, pick the most-cited, and email a 2-sentence summary to me@org.com." -) - -print(f"\n{r.message}\n") -for t in r.tool_executions: - print(f" → {t.tool_name}({list(t.arguments.keys())})") -print(f"\niterations: {r.metrics.iterations} tools: {len(r.tool_executions)}") diff --git a/examples/demos/build-an-agent.gif b/examples/demos/build-an-agent.gif deleted file mode 100644 index e6d6663..0000000 Binary files a/examples/demos/build-an-agent.gif and /dev/null differ diff --git a/examples/demos/build-an-agent.tape b/examples/demos/build-an-agent.tape deleted file mode 100644 index d8099a2..0000000 --- a/examples/demos/build-an-agent.tape +++ /dev/null @@ -1,41 +0,0 @@ -# Locus — three tools, idempotent write, real ReAct loop, on screen. -# -# Compile: vhs build-an-agent.tape → build-an-agent.gif -# -# Pre-reqs to recompile: -# pip install locus[oci] -# ~/.oci/config with at least one GenAI-enabled profile -# export OCI_PROFILE= -# brew install bat # syntax-highlighted code reveal - -Output build-an-agent.gif -Set FontSize 14 -Set Width 1300 -Set Height 1000 -Set Padding 20 -Set Theme "Catppuccin Mocha" -Set TypingSpeed 8ms -Set Shell "bash" - -# --- setup (hidden) ---------------------------------------------------------- -Hide -Type "cd /tmp/locus-gifs/editor-demo" Enter -Type "export PATH=/Users/federico.kamelhar/Projects/locus/.venv/bin:$PATH" Enter -Type "export OCI_PROFILE=LUIGI_FRA_API" Enter -Type "clear" Enter -Sleep 300ms -Show - -# --- 1) reveal the program --------------------------------------------------- -# bat shows the file syntax-highlighted in one shot — no character-by-char -# typing. The viewer sees the whole module at once. -Type "bat --paging=never --style=numbers,header --color=always agent.py" -Enter -Sleep 7500ms - -# --- 2) run it --------------------------------------------------------------- -Type "python agent.py" -Enter -Sleep 11000ms - -Sleep 2500ms diff --git a/examples/demos/hero/.gitignore b/examples/demos/hero/.gitignore deleted file mode 100644 index 35cb39a..0000000 --- a/examples/demos/hero/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# record.py outputs — committed canonically at docs/img/hero.{gif,mp4}. -# These are regenerated by `python record.py`. -frames/ -palette.png -hero.gif -hero.mp4 diff --git a/examples/demos/hero/README.md b/examples/demos/hero/README.md deleted file mode 100644 index cd0985d..0000000 --- a/examples/demos/hero/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Hero animation - -The 44-second hero shown at the top of the project README is generated -from a single HTML page and recorded with Playwright + ffmpeg. It is -not a screen capture — every frame is deterministic, so the file size -is small and the animation is crisp at any resolution. - -## Files - -- [`hero.html`](hero.html) — the page (CSS animations, no JS). -- [`record.py`](record.py) — Playwright loads the page, takes 24-fps - PNG frames for 44 s, ffmpeg stitches into MP4, then a palettegen - pass produces the 14-fps GIF. - -The compiled outputs live at: - -- [`docs/img/hero.gif`](../../../docs/img/hero.gif) (~1.5 MB) -- [`docs/img/hero.mp4`](../../../docs/img/hero.mp4) (~450 KB — preferred for sites that play video) - -## Re-render - -```bash -pip install playwright -python -m playwright install chromium -brew install ffmpeg - -cd examples/demos/hero -python record.py -``` - -Outputs `hero.mp4` and `hero.gif` next to the script. - -## What it shows - -| Beat | Time | Content | -|---|---|---| -| Title | 0–5 s | Locus mark + "Build agents that finish." + service badges | -| Code | 5–24 s | `demo.py` snippet, three red-bordered callouts pulsing in turn over `OracleVectorStore`, `idempotent=True`, `reflexion=True` | -| Run | 23–41 s | Terminal trace: chain-of-thought per iteration, real Oracle 26ai rows with scores, Reflexion confidence, idempotent email, green RESULT box | -| Outcome | 36–44 s | "✓ Brief sent. 4 iterations · 3 tools · 5 services · powered by locus on Oracle 26ai" | - -## Editing - -Adjust timings in `hero.html` (`animation-delay` on each `.hl-*`, -`.co-*`, `.code-stage`, `.term-stage`, `.victory`) and update -`DURATION_MS` in `record.py` to match the longest delay + buffer. diff --git a/examples/demos/hero/hero.html b/examples/demos/hero/hero.html deleted file mode 100644 index 9fb7c5e..0000000 --- a/examples/demos/hero/hero.html +++ /dev/null @@ -1,355 +0,0 @@ - - - - - Locus hero - - - - -
- -
- - - - - - -

locus

-
Build agents that finish.
-
- Oracle 26ai · vector search - OCI GenAI · gpt-5.5 - OCI bucket · checkpointer -
-
- - -
-
-
- - - - demo.py -
-
from locus import Agent -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever -from locus.memory.backends.oci_bucket import OCIBucketBackend -from locus.tools.decorator import tool - -## tools -@tool -def search_corpus(topic: str, limit: int = 3) -> list[dict]: - rs = asyncio.run(retriever.retrieve(topic, limit=limit)) - return [{"id": r.document.id, "content": r.document.content, - "score": round(r.score, 3)} for r in rs.documents] - -@tool(idempotent=True) -def email_report(to, subject, body): ... - -## agent -agent = Agent( - model="oci:openai.gpt-5.5", - tools=[search_corpus, email_report], - skills=[Skill.from_file("skills/researcher")], - reflexion=True, - checkpointer=OCIBucketBackend(bucket_name=..., namespace=...), -) -agent.run("Brief me on HNSW. Cite top 3. Email me.")
-
-
Oracle 26ai native VECTOR similarity
-
no double-charges
-
self-correcting agent
-
- - -
-
-
- $ python demo.py - live · Oracle 26ai · OCI GenAI · OCI bucket -
-
-
Oracle AI Database 26ai Enterprise Edition Release 23.26.2.1.0
-
LOCUS_DEMO_DOCS: 5 rows · VECTOR(1024, FLOAT32)
-
💭 [iter 1] thinking: I'll load the researcher skill so I can follow its corpus-grounded workflow.
-
🔧 skills(skill_name='researcher')
-
↻ reflexion: new_findings (confidence 0.15)
-
💭 [iter 2] thinking: I'll search the corpus for HNSW to find the top three relevant papers.
-
🔧 search_corpus(topic='HNSW', limit=3)
-
↳ Oracle 26ai → id=hnsw score=0.799
-
↳ Oracle 26ai → id=embeddings score=0.565
-
↳ Oracle 26ai → id=ivf score=0.558
-
↻ reflexion: new_findings (confidence 0.26)
-
💭 [iter 3] thinking: I'll email the 2-sentence corpus-grounded HNSW summary now.
-
🔧 email_report(to='me@org.com', subject='HNSW brief')
-
↳ email mock → 'me@org.com' (540 chars, idempotent)
-
↻ reflexion: on_track (confidence 0.34)
- -
-
EMAIL BODY · sent to me@org.com
-
- Subject: HNSW brief

- HNSW (Hierarchical Navigable Small World) is a graph-based - approximate nearest-neighbor index that traverses a multi-layer - graph from coarse to fine, finding similar vectors in - logarithmic time. The corpus cited the seminal HNSW paper - (id=hnsw, 2018), the embeddings primer (id=embeddings, 2024), - and the IVF baseline (id=ivf, 2017). -
-
-
-
-
- - -
-
- - Brief sent. -
-
- 4 iterations · 3 tools · 5 services · powered by locus on Oracle 26ai -
-
-
- - diff --git a/examples/demos/hero/intro.html b/examples/demos/hero/intro.html deleted file mode 100644 index 7a05a4a..0000000 --- a/examples/demos/hero/intro.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - Locus intro - - - - -
- -
- - diff --git a/examples/demos/hero/record.py b/examples/demos/hero/record.py deleted file mode 100644 index f38b53a..0000000 --- a/examples/demos/hero/record.py +++ /dev/null @@ -1,128 +0,0 @@ -# ruff: noqa: ASYNC221, S603, S607 -"""Render hero.html → frames → MP4 via Playwright + ffmpeg. - -This is a build script for the GIF embedded at the top of the project -README. Calls ffmpeg via subprocess; that's intentional and the inputs -are statically defined paths. -""" - -from __future__ import annotations - -import asyncio -import os -import shutil -import subprocess -from pathlib import Path - -from playwright.async_api import async_playwright - - -HERE = Path(__file__).parent -HERO = HERE / "hero.html" -FRAMES = HERE / "frames" -OUT_MP4 = HERE / "hero.mp4" -OUT_GIF = HERE / "hero.gif" - -DURATION_MS = 44000 # match the longest CSS animation -FPS = 24 -FRAME_INTERVAL_MS = int(1000 / FPS) - - -async def main() -> None: - if FRAMES.exists(): - shutil.rmtree(FRAMES) - FRAMES.mkdir() - - async with async_playwright() as p: - browser = await p.chromium.launch() - ctx = await browser.new_context( - viewport={"width": 1280, "height": 720}, - device_scale_factor=2, - ) - page = await ctx.new_page() - - # Pause CSS animations so we can step them deterministically. - await page.add_init_script(""" - window.__step = 0; - """) - - await page.goto(f"file://{HERO.resolve()}") - await page.wait_for_load_state("networkidle") - - # Force animations to use a virtual clock instead of real time. - # We'll use page.clock controls if available; otherwise just sleep - # in a tight loop and screenshot. - await page.emulate_media(reduced_motion=None) - - n_frames = DURATION_MS // FRAME_INTERVAL_MS - for i in range(n_frames): - path = FRAMES / f"frame_{i:05d}.png" - await page.screenshot(path=str(path), full_page=False) - await page.wait_for_timeout(FRAME_INTERVAL_MS) - - await ctx.close() - await browser.close() - - # Stitch into MP4. - if OUT_MP4.exists(): - OUT_MP4.unlink() - subprocess.check_call( - [ - "ffmpeg", - "-y", - "-loglevel", - "error", - "-framerate", - str(FPS), - "-i", - str(FRAMES / "frame_%05d.png"), - "-c:v", - "libx264", - "-pix_fmt", - "yuv420p", - "-vf", - "scale=1280:720,format=yuv420p", - "-movflags", - "+faststart", - str(OUT_MP4), - ] - ) - - # MP4 → GIF (palette-optimised, ~12 fps, scaled to 1080p width). - if OUT_GIF.exists(): - OUT_GIF.unlink() - palette = HERE / "palette.png" - subprocess.check_call( - [ - "ffmpeg", - "-y", - "-loglevel", - "error", - "-i", - str(OUT_MP4), - "-vf", - "fps=10,scale=820:-1:flags=lanczos,palettegen=stats_mode=diff:max_colors=96", - str(palette), - ] - ) - subprocess.check_call( - [ - "ffmpeg", - "-y", - "-loglevel", - "error", - "-i", - str(OUT_MP4), - "-i", - str(palette), - "-lavfi", - "fps=10,scale=820:-1:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3", - str(OUT_GIF), - ] - ) - print(f"Wrote {OUT_MP4} ({OUT_MP4.stat().st_size // 1024} KiB)") - print(f"Wrote {OUT_GIF} ({OUT_GIF.stat().st_size // 1024} KiB)") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/hero/record_intro.py b/examples/demos/hero/record_intro.py deleted file mode 100644 index 303b266..0000000 --- a/examples/demos/hero/record_intro.py +++ /dev/null @@ -1,72 +0,0 @@ -# ruff: noqa: ASYNC221, S603, S607 -"""Render intro.html → 6-second MP4 via Playwright.""" - -from __future__ import annotations - -import asyncio -import shutil -import subprocess -from pathlib import Path - -from playwright.async_api import async_playwright - - -HERE = Path(__file__).parent -PAGE = HERE / "intro.html" -FRAMES = HERE / "frames" -OUT_MP4 = HERE / "intro.mp4" - -DURATION_MS = 6200 # 6 s logo + buffer -FPS = 24 -FRAME_INTERVAL_MS = int(1000 / FPS) - - -async def main() -> None: - if FRAMES.exists(): - shutil.rmtree(FRAMES) - FRAMES.mkdir() - - async with async_playwright() as p: - browser = await p.chromium.launch() - ctx = await browser.new_context( - viewport={"width": 1500, "height": 800}, - device_scale_factor=2, - ) - page = await ctx.new_page() - await page.goto(f"file://{PAGE.resolve()}") - await page.wait_for_load_state("networkidle") - - n = DURATION_MS // FRAME_INTERVAL_MS - for i in range(n): - await page.screenshot(path=str(FRAMES / f"frame_{i:05d}.png")) - await page.wait_for_timeout(FRAME_INTERVAL_MS) - - await ctx.close() - await browser.close() - - subprocess.check_call( - [ - "ffmpeg", - "-y", - "-loglevel", - "error", - "-framerate", - str(FPS), - "-i", - str(FRAMES / "frame_%05d.png"), - "-c:v", - "libx264", - "-pix_fmt", - "yuv420p", - "-vf", - "scale=1500:800,format=yuv420p", - "-movflags", - "+faststart", - str(OUT_MP4), - ] - ) - print(f"Wrote {OUT_MP4} ({OUT_MP4.stat().st_size // 1024} KiB)") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/hero/record_scenes.py b/examples/demos/hero/record_scenes.py deleted file mode 100644 index c47b14a..0000000 --- a/examples/demos/hero/record_scenes.py +++ /dev/null @@ -1,72 +0,0 @@ -# ruff: noqa: ASYNC221, S603, S607 -"""Render scenes.html → ~14-second MP4 via Playwright.""" - -from __future__ import annotations - -import asyncio -import shutil -import subprocess -from pathlib import Path - -from playwright.async_api import async_playwright - - -HERE = Path(__file__).parent -PAGE = HERE / "scenes.html" -FRAMES = HERE / "scene_frames" -OUT_MP4 = HERE / "scenes.mp4" - -DURATION_MS = 21200 -FPS = 24 -FRAME_INTERVAL_MS = int(1000 / FPS) - - -async def main() -> None: - if FRAMES.exists(): - shutil.rmtree(FRAMES) - FRAMES.mkdir() - - async with async_playwright() as p: - browser = await p.chromium.launch() - ctx = await browser.new_context( - viewport={"width": 1500, "height": 800}, - device_scale_factor=2, - ) - page = await ctx.new_page() - await page.goto(f"file://{PAGE.resolve()}") - await page.wait_for_load_state("networkidle") - - n = DURATION_MS // FRAME_INTERVAL_MS - for i in range(n): - await page.screenshot(path=str(FRAMES / f"frame_{i:05d}.png")) - await page.wait_for_timeout(FRAME_INTERVAL_MS) - - await ctx.close() - await browser.close() - - subprocess.check_call( - [ - "ffmpeg", - "-y", - "-loglevel", - "error", - "-framerate", - str(FPS), - "-i", - str(FRAMES / "frame_%05d.png"), - "-c:v", - "libx264", - "-pix_fmt", - "yuv420p", - "-vf", - "scale=1500:800,format=yuv420p", - "-movflags", - "+faststart", - str(OUT_MP4), - ] - ) - print(f"Wrote {OUT_MP4} ({OUT_MP4.stat().st_size // 1024} KiB)") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/hero/scenes.html b/examples/demos/hero/scenes.html deleted file mode 100644 index 4bd6038..0000000 --- a/examples/demos/hero/scenes.html +++ /dev/null @@ -1,277 +0,0 @@ - - - - - Locus scenes - - - - -
- - -
-

THE PROBLEM

-
- you: "plan my Tokyo trip 🗼"
- one agent: books 47 restaurants 🍣🍣🍣
- sends 8 itineraries to your boss 📧
- charges your card ¥240 000 💸
- calls your mom 📞 -
-
- One agent trying to do everything is the "please call me back" of AI.
- Three agents that talk to each other first just plan the trip. -
-
- - -
-
- Live stream - A2A graph - Tool calls - Final verdict - Locus · Tokyo trip team -
- -
- - - - Parallel specialists - - HUMAN-IN-LOOP - 3 agents · 5 LLM rounds · 2 idempotent writes -
- Two specialists query Oracle 26ai, debate picks for two rounds, hand off the - agreed itinerary to a concierge that books exactly once and emails once — only - after you approve. -
-
- -
-
SIGNAL
-
-
Vector store
Oracle 26ai · TOKYO_TRIP_RECS · VECTOR(1024) · COSINE
-
Embeddings
oci.cohere.embed-english-v3.0
-
Chat model
oci.openai.gpt-5.5
-
Checkpointer
oci-bucket://yzhbfkqxqsx9/locus-test-checkpoints
-
Specialists
foodie · culture
-
Concierge writes
book_restaurant · email_itinerary (both @tool(idempotent=True))
-
-
- -
-
-

Live event stream

- 7 events · 0:00–1:42 -
-
-
trip plan_id = trip-29df10a · sequential dispatch

-
foodie.search_food → Oracle 26ai · VECTOR_DISTANCE → 4 rows
-
culture.search_culture → Oracle 26ai · VECTOR_DISTANCE → 4 rows
-
foodie ↔ culture debate · 2 rounds · agreed
-
human.consent ⏸ waits on input("Approve? [y/N]")
-
concierge.book_restaurant idempotent · res #R-20453
-
concierge.email_itinerary idempotent · 504 chars · me@org.com
-
-
-
-
- - diff --git a/examples/demos/oracle_26ai/README.md b/examples/demos/oracle_26ai/README.md deleted file mode 100644 index 6f48246..0000000 --- a/examples/demos/oracle_26ai/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Oracle 26ai end-to-end demo - -A real, runnable agent that exercises every layer of the locus stack -against live Oracle services. No mocks except the email tool, which -falls back to a mock send when Gmail credentials aren't set. - -## What it shows - -| Layer | Service | Locus class | -|---|---|---| -| Reasoning | OCI GenAI (`openai.gpt-5.5`) | `Agent(reflexion=True)` | -| Skill loading | Filesystem | `Skill.from_file(...)` | -| Embeddings | OCI GenAI (`cohere.embed-english-v3.0`) | `OCIEmbeddings` | -| Vector retrieval | **Oracle 26ai** native `VECTOR` | `OracleVectorStore` | -| Idempotent write | `@tool(idempotent=True)` | `email_report` | -| Durable memory | OCI Object Storage | `OCIBucketBackend` | - -## Files - -- [`demo.py`](demo.py) — the agent program. ~125 lines. -- [`setup_corpus.py`](setup_corpus.py) — one-shot ingest of five - sample documents. Idempotent: re-running is a no-op if the - table is populated. -- [`skills/researcher/SKILL.md`](skills/researcher/SKILL.md) — the - AgentSkills.io-compliant skill the agent loads. -- [`demo.gif`](demo.gif) — recorded run against the live free-tier - ADB. - -## Pre-reqs - -```bash -pip install "locus[oci,oracle]" -``` - -You need: - -- An OCI tenancy with [GenAI service](https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm) - in `us-chicago-1` (or another GenAI region). -- An [Autonomous Database 26ai](https://docs.oracle.com/en-us/iaas/autonomous-database-shared/index.html) - with the wallet downloaded locally. -- An OCI Object Storage bucket for checkpoints (or change the demo - to use any other locus checkpointer backend — see - [`docs/concepts/checkpointers.md`](../../../docs/concepts/checkpointers.md)). - -## Configuration (env vars) - -| Variable | Default | Description | -|---|---|---| -| `OCI_PROFILE` | `DEFAULT` | Profile in `~/.oci/config` | -| `OCI_GENAI_REGION` | `us-chicago-1` | Region for GenAI inference | -| `OCI_NAMESPACE` | *required* | Object Storage namespace | -| `OCI_BUCKET_NAME` | `locus-test-checkpoints` | Checkpointer bucket | -| `ORACLE_DSN` | `deepresearch_low` | TNS alias from your wallet | -| `ORACLE_USER` | `ADMIN` | DB user | -| `ORACLE_PASSWORD` | *required* | DB password | -| `ORACLE_WALLET` | `~/.oci/wallets/deepresearch` | Wallet directory | -| `ORACLE_TABLE` | `LOCUS_DEMO_DOCS` | Vector table name | -| `GMAIL_USER` | *(unset → mock)* | SMTP login | -| `GMAIL_APP_PASSWORD` | *(unset → mock)* | Gmail [App Password](https://myaccount.google.com/apppasswords) | - -## Run it - -```bash -# 1. Set the required env vars -export OCI_PROFILE=DEFAULT -export OCI_NAMESPACE= -export ORACLE_PASSWORD= -export ORACLE_WALLET=$HOME/.oci/wallets/ - -# 2. One-time corpus ingest -python setup_corpus.py - -# 3. Run the agent -python demo.py -``` - -Expected output: - -``` -→ Oracle AI Database 26ai Enterprise Edition Release 23.26.2.1.0 - Production -→ LOCUS_DEMO_DOCS: 5 rows · VECTOR(1024, FLOAT32) - - -💭 [iter 1] plan: skills -🔧 skills(skill_name='researcher') -↻ reflexion: new_findings (confidence 0.15) - -💭 [iter 2] plan: search_corpus -🔧 search_corpus(topic='HNSW', limit=3) - ↳ Oracle 26ai → id=hnsw score=0.799 - ↳ Oracle 26ai → id=embeddings score=0.565 - ↳ Oracle 26ai → id=ivf score=0.558 -↻ reflexion: new_findings (confidence 0.26) - -💭 [iter 3] plan: email_report -🔧 email_report(to='me@org.com', subject='HNSW brief', body='…') - ↳ email mock → 'me@org.com' (545 chars) -↻ reflexion: on_track (confidence 0.34) - -✓ Sent a 2-sentence HNSW summary citing "hnsw," "embeddings," and "ivf" to me@org.com. -``` - -## Re-record the GIF - -The GIF was made with [VHS](https://github.com/charmbracelet/vhs): - -```bash -brew install vhs neovim -cd examples/demos/oracle_26ai -vhs demo.tape -``` - -`demo.tape` is the source — fork it for your own walkthrough. diff --git a/examples/demos/oracle_26ai/demo.gif b/examples/demos/oracle_26ai/demo.gif deleted file mode 100644 index a25b566..0000000 Binary files a/examples/demos/oracle_26ai/demo.gif and /dev/null differ diff --git a/examples/demos/oracle_26ai/demo.py b/examples/demos/oracle_26ai/demo.py deleted file mode 100644 index cc3eca5..0000000 --- a/examples/demos/oracle_26ai/demo.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Locus + Oracle 26ai — end-to-end demo. - -What this program does in one ``agent.run()``: - • loads the ``researcher`` skill from disk - • search_corpus → Oracle 26ai native VECTOR similarity - • Reflexion self-evaluation each iteration - • email_report is @tool(idempotent=True) - • every step checkpointed to OCI Object Storage - -Everything is env-driven — no hardcoded credentials or paths. -""" - -from __future__ import annotations - -import asyncio -import json -import os -import smtplib -import textwrap -import uuid -from email.mime.text import MIMEText -from pathlib import Path - -import oracledb - -from locus import Agent -from locus.core.events import ( - ReflectEvent, - TerminateEvent, - ThinkEvent, - ToolCompleteEvent, - ToolStartEvent, -) -from locus.memory.backends.oci_bucket import OCIBucketBackend -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever -from locus.skills import Skill -from locus.tools.decorator import tool - - -# ─── Configuration ───────────────────────────────────────────────────────── -# OCI -PROFILE = os.environ.get("OCI_PROFILE", "DEFAULT") -GENAI_REGION = os.environ.get("OCI_GENAI_REGION", "us-chicago-1") -GENAI_ENDPOINT = f"https://inference.generativeai.{GENAI_REGION}.oci.oraclecloud.com" - -# Oracle 26ai -ORACLE_DSN = os.environ.get("ORACLE_DSN", "deepresearch_low") -ORACLE_USER = os.environ.get("ORACLE_USER", "ADMIN") -ORACLE_PW = os.environ["ORACLE_PASSWORD"] -ORACLE_WALLET = os.environ.get("ORACLE_WALLET", os.path.expanduser("~/.oci/wallets/deepresearch")) -TABLE = os.environ.get("ORACLE_TABLE", "LOCUS_DEMO_DOCS") - -# OCI Object Storage (checkpointer) -BUCKET = os.environ.get("OCI_BUCKET_NAME", "locus-test-checkpoints") -NAMESPACE = os.environ["OCI_NAMESPACE"] - - -# ─── Vector store + embeddings ───────────────────────────────────────────── -retriever = RAGRetriever( - embedder=OCIEmbeddings( - model_id="cohere.embed-english-v3.0", - profile_name=PROFILE, - service_endpoint=GENAI_ENDPOINT, - ), - store=OracleVectorStore( - dsn=ORACLE_DSN, - user=ORACLE_USER, - password=ORACLE_PW, - wallet_location=ORACLE_WALLET, - wallet_password=ORACLE_PW, - dimension=1024, - table_name=TABLE, - ), -) - - -# ─── Tools ───────────────────────────────────────────────────────────────── -@tool -def search_corpus(topic: str, limit: int = 3) -> list[dict]: - """Search the Oracle 26ai corpus.""" - rs = asyncio.run(retriever.retrieve(topic, limit=limit)) - return [ - {"id": r.document.id, "content": r.document.content, "score": round(r.score, 3)} - for r in rs.documents - ] - - -@tool(idempotent=True) -def email_report(to: str, subject: str, body: str) -> dict: - """Send the brief. Idempotent — re-fires return the cached receipt.""" - user, pw = os.environ.get("GMAIL_USER"), os.environ.get("GMAIL_APP_PASSWORD") - if user and pw: - msg = MIMEText(body) - msg["Subject"], msg["From"], msg["To"] = subject, user, to - with smtplib.SMTP_SSL("smtp.gmail.com", 465) as s: - s.login(user, pw) - s.sendmail(user, [to], msg.as_string()) - return {"via": "gmail", "to": to, "chars": len(body)} - return {"via": "mock", "to": to, "chars": len(body)} - - -# ─── Agent ───────────────────────────────────────────────────────────────── -agent = Agent( - model="oci:openai.gpt-5.5", - tools=[search_corpus, email_report], - skills=[Skill.from_file(Path(__file__).parent / "skills" / "researcher")], - reflexion=True, - checkpointer=OCIBucketBackend( - bucket_name=BUCKET, - namespace=NAMESPACE, - profile_name=PROFILE, - ), - system_prompt=( - "You are a research assistant. Before every tool call, write one " - "short sentence explaining what you're about to do and why. " - "Then call the tool. Use the available skill." - ), -) - - -# ─── Run ─────────────────────────────────────────────────────────────────── -def preflight() -> None: - """Open a real Oracle 26ai connection so the version banner is visible.""" - with oracledb.connect( - user=ORACLE_USER, - password=ORACLE_PW, - dsn=ORACLE_DSN, - config_dir=ORACLE_WALLET, - wallet_location=ORACLE_WALLET, - wallet_password=ORACLE_PW, - ) as conn: - cur = conn.cursor() - cur.execute("SELECT banner_full FROM v$version") - banner = cur.fetchone()[0].splitlines()[0] - cur.execute(f"SELECT count(*) FROM {TABLE}") # noqa: S608 — TABLE is internal config - rows = cur.fetchone()[0] - print(f"→ {banner}") - print(f"→ {TABLE}: {rows} rows · VECTOR(1024, FLOAT32)") - print() - - -async def main() -> None: - preflight() - prompt = ( - "Brief me on HNSW. Use my research corpus, cite the top three papers, " - "then email a 2-sentence summary to me@org.com." - ) - thread_id = f"demo-{uuid.uuid4().hex[:8]}" - - async for event in agent.run(prompt, thread_id=thread_id): - match event: - case ThinkEvent(iteration=i, reasoning=r, tool_calls=calls): - if r: - print(f"\n💭 [iter {i}] thinking: {r.strip()}") - if calls: - print(f" plan → {', '.join(c.name for c in calls)}") - case ToolStartEvent(tool_name="email_report", arguments=a): - print(f"🔧 email_report(to={a.get('to')!r}, subject={a.get('subject')!r})") - print(f" ┌── EMAIL BODY ──────────────────────────────────────────") - for line in textwrap.wrap(a.get("body", ""), width=70): - print(f" │ {line}") - print(f" └────────────────────────────────────────────────────────") - case ToolStartEvent(tool_name=n, arguments=a): - args = ", ".join(f"{k}={v!r}" for k, v in a.items())[:80] - print(f"🔧 {n}({args})") - case ToolCompleteEvent(tool_name="search_corpus", result=r) if r: - for row in json.loads(r): - print(f" ↳ Oracle 26ai → id={row['id']:<10} score={row['score']:.3f}") - case ToolCompleteEvent(tool_name="email_report", result=r) if r: - d = json.loads(r) - print(f" ↳ email {d['via']} → {d['to']!r} ({d['chars']} chars)") - case ReflectEvent(assessment=a, new_confidence=c): - print(f"↻ reflexion: {a} (confidence {c:.2f})") - case TerminateEvent(final_message=m): - print(f"\n✓ {m}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/oracle_26ai/demo.tape b/examples/demos/oracle_26ai/demo.tape deleted file mode 100644 index 5ae9879..0000000 --- a/examples/demos/oracle_26ai/demo.tape +++ /dev/null @@ -1,50 +0,0 @@ -# Locus — Oracle 26ai end-to-end demo (real services, narrated) -# -# Three scenes: -# 1. View the skill in nvim (key terms highlighted). -# 2. Walk the agent program in nvim (key terms highlighted, slow pages). -# 3. Run it. Watch the chain of thought, the live Oracle 26ai rows, -# Reflexion, the idempotent email, and the final reply. - -Output demo.gif -Set FontSize 18 -Set Width 1500 -Set Height 1280 -Set Padding 24 -Set Theme "Catppuccin Mocha" -Set TypingSpeed 18ms -Set Shell "bash" - -Hide -Type "cd $(mktemp -d) && clear" Enter -Sleep 200ms -Show - -# --- Scene 1: skill in nvim with keyword highlighting ---------------------- -Type `nvim -R -c "set hlsearch" -c "let @/='search_corpus\|email_report\|idempotent\|Skill\|Loop'" /Users/federico.kamelhar/Projects/locus/examples/demos/oracle_26ai/skills/researcher/SKILL.md` -Enter -Sleep 14000ms -Type ":q" -Enter -Sleep 400ms - -# --- Scene 2: agent program in nvim with keyword highlighting -------------- -Type `nvim -R -c "set hlsearch" -c "let @/='@tool\|idempotent\|reflexion\|skills\|search_corpus\|Agent\|OracleVectorStore\|OCIBucketBackend\|ThinkEvent'" /Users/federico.kamelhar/Projects/locus/examples/demos/oracle_26ai/demo.py` -Enter -Sleep 3000ms -PageDown -Sleep 9000ms -PageDown -Sleep 9000ms -PageDown -Sleep 9000ms -Type ":q" -Enter -Sleep 400ms - -# --- Scene 3: live run ----------------------------------------------------- -Type "python /Users/federico.kamelhar/Projects/locus/examples/demos/oracle_26ai/demo.py" -Enter -Sleep 28000ms - -Sleep 6000ms diff --git a/examples/demos/oracle_26ai/setup_corpus.py b/examples/demos/oracle_26ai/setup_corpus.py deleted file mode 100644 index 0e73cda..0000000 --- a/examples/demos/oracle_26ai/setup_corpus.py +++ /dev/null @@ -1,108 +0,0 @@ -"""One-shot ingest of the demo corpus into Oracle 26ai. - -Run once before ``demo.py``. Idempotent — re-running just no-ops if the -table already has data. - -Required env vars: - OCI_PROFILE — OCI config profile (default API_FREE_TIER) - ORACLE_PASSWORD — ADB ADMIN password - ORACLE_WALLET — wallet directory (default ~/.oci/wallets/deepresearch) - ORACLE_DSN — TNS alias (default deepresearch_low) -""" - -from __future__ import annotations - -import asyncio -import os - -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever - - -CORPUS = [ - ( - "hnsw", - "Hierarchical Navigable Small World (HNSW) is a graph-based " - "approximate nearest-neighbor index. Each node connects to neighbors " - "at multiple layers; queries descend from the top layer to local " - "neighborhoods. Search is logarithmic in corpus size and routinely " - "beats inverted-file methods on recall at the same latency. Malkov " - "and Yashunin published the seminal paper in 2018.", - ), - ( - "ivf", - "Inverted-file (IVF) indexes partition the vector space into Voronoi " - "cells and search only the cells closest to the query. They trade " - "recall for throughput: smaller `nprobe` is faster but less accurate. " - "Faiss popularised IVF on GPUs; Oracle 26ai supports IVF via " - "ORGANIZATION NEIGHBOR PARTITIONS for billion-scale workloads.", - ), - ( - "rag", - "Retrieval-Augmented Generation grounds an LLM in an external " - "corpus by retrieving relevant passages at query time and prepending " - "them to the prompt. Lewis et al. (NeurIPS 2020) introduced the term. " - "Modern systems chunk at 500-1000 tokens, embed with a strong " - "encoder, and store in a vector index — exactly the pipeline this " - "demo runs.", - ), - ( - "embeddings", - "Embedding models map text to dense vectors where semantic " - "similarity corresponds to cosine distance. Cohere's " - "embed-english-v3 produces 1024-dim vectors and is hosted on OCI " - "GenAI. Larger dimensions cost more storage and search time; 1024 is " - "the sweet spot for most retrieval workloads.", - ), - ( - "reflexion", - "Reflexion (Shinn et al., 2023) lets an agent self-evaluate after " - "each iteration: did my last step make progress? If not, the agent " - "revises its approach instead of stacking another tool call on top " - "of a wrong premise. Locus exposes Reflexion as `reflexion=True` on " - "Agent — no separate library, no agent rewrite.", - ), -] - - -async def main(): - profile = os.environ.get("OCI_PROFILE", "API_FREE_TIER") - wallet = os.environ.get("ORACLE_WALLET", os.path.expanduser("~/.oci/wallets/deepresearch")) - pw = os.environ["ORACLE_PASSWORD"] - - # Tenancy root is fine as the compartment for free-tier accounts. - embedder = OCIEmbeddings( - model_id="cohere.embed-english-v3.0", - profile_name=profile, - compartment_id=os.environ.get( - "OCI_COMPARTMENT", - "ocid1.tenancy.oc1..aaaaaaaaqlhpnytg33ztkwrdpq62p5yxx5gn5ltmkah23m7qebwjzc7x3lcq", - ), - service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - ) - - store = OracleVectorStore( - dsn=os.environ.get("ORACLE_DSN", "deepresearch_low"), - user="ADMIN", - password=pw, - wallet_location=wallet, - wallet_password=pw, - dimension=1024, - table_name="LOCUS_DEMO_DOCS", - ) - - retriever = RAGRetriever(embedder=embedder, store=store) - - already = await retriever.retrieve("HNSW", limit=1) - if already.documents: - print(f"Corpus already populated ({len(CORPUS)} docs) — skipping ingest.") - return - - print(f"Ingesting {len(CORPUS)} documents into Oracle 26ai…") - for doc_id, content in CORPUS: - await retriever.add_document(content, doc_id=doc_id, chunk=False) - print(f" + {doc_id}") - print("Done.") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/oracle_26ai/skills/researcher/SKILL.md b/examples/demos/oracle_26ai/skills/researcher/SKILL.md deleted file mode 100644 index 9971dee..0000000 --- a/examples/demos/oracle_26ai/skills/researcher/SKILL.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: researcher -description: Use this skill when answering a research question by grounding in a corpus. Searches the corpus first, ranks by relevance, summarises in two sentences, and emails a brief. -allowed-tools: search_corpus, email_report -metadata: - author: locus-demo - version: "1.0" ---- - -# Researcher - -You are a research analyst. Every answer is grounded in the corpus. - -## Loop - -1. Always call `search_corpus(topic)` **first** — never answer from memory alone. -2. Pick the most-cited / highest-scoring document. -3. Summarise in **two sentences**, citing the paper title. -4. Call `email_report(to, subject, body)` **exactly once**. The tool is - idempotent — re-fires return the cached receipt, so a transient network - blip won't double-send. -5. Reply to the user with one sentence: what you sent, to whom. - -## Style - -- Terse. No "as a research analyst…" preambles. -- Cite paper titles in quotes. -- Never invent papers that didn't come back from `search_corpus`. diff --git a/examples/demos/po_approval/.gitignore b/examples/demos/po_approval/.gitignore deleted file mode 100644 index 7e11b8f..0000000 --- a/examples/demos/po_approval/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Generated media — re-render with `python po_approval.py` + the .tape file. -*.mp4 -*.gif diff --git a/examples/demos/po_approval/_chat/.gitignore b/examples/demos/po_approval/_chat/.gitignore deleted file mode 100644 index c2496e4..0000000 --- a/examples/demos/po_approval/_chat/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Local-only render assets — chat-bubble screenshot for slides / decks. -# Source HTML lives here but is not committed; regenerate with: -# python snap.py -* -!.gitignore diff --git a/examples/demos/po_approval/po_approval.py b/examples/demos/po_approval/po_approval.py deleted file mode 100644 index f1f82f3..0000000 --- a/examples/demos/po_approval/po_approval.py +++ /dev/null @@ -1,402 +0,0 @@ -# ruff: noqa: ASYNC250, F841, ASYNC221, S603, S607 -"""Three locus agents collaborate on a vendor purchase-order approval. - -A real enterprise multi-agent workflow: - - 1. Procurement — searches Oracle 26ai for vendors fitting the spend. - 2. Compliance — searches the same corpus for SOC2 / ISO certifications. - 3. Procurement ↔ Compliance — debate trade-offs (cost vs compliance). - 4. Approval Officer — receives the joint recommendation, asks the human - user for consent, then fires: - • submit_po (@tool(idempotent=True)) - • email_cfo (@tool(idempotent=True)) - Both writes are deduped. The thread is checkpointed - to OCI Object Storage on every iteration. - -Required env: - OCI_PROFILE (default DEFAULT) - ORACLE_PASSWORD (required) - ORACLE_WALLET (default ~/.oci/wallets/deepresearch) - OCI_NAMESPACE (required — for the OCI bucket checkpointer) - OCI_BUCKET_NAME (default locus-test-checkpoints) -""" - -from __future__ import annotations - -import asyncio -import os -import sys -import textwrap -import time -import uuid - -import oracledb - -from locus import Agent -from locus.core.events import ( - ModelChunkEvent, - TerminateEvent, - ToolStartEvent, -) -from locus.memory.backends.oci_bucket import OCIBucketBackend -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever -from locus.tools.decorator import tool - - -# ─── Config ──────────────────────────────────────────────────────────────── -PROFILE = os.environ.get("OCI_PROFILE", "DEFAULT") -ORACLE_PW = os.environ["ORACLE_PASSWORD"] -WALLET = os.environ.get("ORACLE_WALLET", os.path.expanduser("~/.oci/wallets/deepresearch")) -ORACLE_DSN = os.environ.get("ORACLE_DSN", "deepresearch_low") -BUCKET = os.environ.get("OCI_BUCKET_NAME", "locus-test-checkpoints") -NAMESPACE = os.environ["OCI_NAMESPACE"] -COMPARTMENT = os.environ.get( - "OCI_COMPARTMENT", - "ocid1.tenancy.oc1..aaaaaaaaqlhpnytg33ztkwrdpq62p5yxx5gn5ltmkah23m7qebwjzc7x3lcq", -) -GENAI_ENDPOINT = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" - - -def _new_retriever() -> RAGRetriever: - """Fresh per-call — async pools can't cross event loops.""" - return RAGRetriever( - embedder=OCIEmbeddings( - model_id="cohere.embed-english-v3.0", - profile_name=PROFILE, - compartment_id=COMPARTMENT, - service_endpoint=GENAI_ENDPOINT, - ), - store=OracleVectorStore( - dsn=ORACLE_DSN, - user="ADMIN", - password=ORACLE_PW, - wallet_location=WALLET, - wallet_password=ORACLE_PW, - dimension=1024, - table_name="VENDOR_CATALOG", - ), - ) - - -# ─── Tools ───────────────────────────────────────────────────────────────── -@tool -def search_vendors(query: str, limit: int = 4) -> list[dict]: - """Search the Oracle 26ai vendor catalogue.""" - rs = asyncio.run(_new_retriever().retrieve(f"vendor {query}", limit=limit)) - return [ - {"id": r.document.id, "content": r.document.content, "score": round(r.score, 3)} - for r in rs.documents - ] - - -@tool -def search_compliance(query: str, limit: int = 4) -> list[dict]: - """Search the same catalogue, prioritising compliance certifications.""" - rs = asyncio.run(_new_retriever().retrieve(f"SOC2 ISO compliance {query}", limit=limit)) - return [ - {"id": r.document.id, "content": r.document.content, "score": round(r.score, 3)} - for r in rs.documents - ] - - -_PO_SUBMITTED: dict[tuple[str, float], dict] = {} -_EMAILS: list[dict] = [] - - -@tool(idempotent=True) -def submit_po(vendor_id: str, amount_usd: float, term_days: int) -> dict: - """Submit the purchase order. Idempotent — re-fires return cached PO.""" - key = (vendor_id, amount_usd) - if key in _PO_SUBMITTED: - return {**_PO_SUBMITTED[key], "cached": True} - po_id = f"PO-{abs(hash(key)) % 100000:05d}" - receipt = { - "status": "submitted", - "po_id": po_id, - "vendor_id": vendor_id, - "amount_usd": amount_usd, - "term_days": term_days, - } - _PO_SUBMITTED[key] = receipt - return receipt - - -@tool(idempotent=True) -def email_cfo(to: str, subject: str, body: str) -> dict: - """Email the CFO with the PO summary. Idempotent.""" - _EMAILS.append({"to": to, "subject": subject, "body": body}) - return {"status": "sent", "to": to, "chars": len(body)} - - -# ─── Three agents ────────────────────────────────────────────────────────── -procurement = Agent( - model="oci:openai.gpt-5.5", - tools=[search_vendors], - system_prompt=( - "You are the Procurement specialist. Call search_vendors EXACTLY ONCE " - "with query='cloud compute storage'. From the returned list pick three " - "candidates that fit a $2M cloud infrastructure budget. Format: 3 " - "bullets — vendor id, annual list, why. Use ONLY ids in the result." - ), - max_iterations=3, -) - -compliance = Agent( - model="oci:openai.gpt-5.5", - tools=[search_compliance], - system_prompt=( - "You are the Compliance specialist. Call search_compliance EXACTLY " - "ONCE with query='SOC2 ISO compliance'. From the returned list pick " - "three vendors with the strongest SOC2 / ISO posture for a regulated " - "workload. Format: 3 bullets — vendor id, certifications, comment. " - "Use ONLY ids in the result." - ), - max_iterations=3, -) - - -def _make_voice(name: str, personality: str) -> Agent: - """Free-form persona for dialogue rounds — no tools, prose only.""" - return Agent( - model="oci:openai.gpt-5.5", - system_prompt=( - f"You are {name}, a member of an enterprise vendor-review team. " - f"{personality} You are in a CONVERSATION with another team " - "member — not answering a request. Reply in 2-3 sentences of " - "plain prose. No bullets. Reference vendor ids when useful." - ), - max_iterations=2, - ) - - -procurement_voice = _make_voice( - "Procurement", - "You care about price, payment terms, and total cost over the contract.", -) -compliance_voice = _make_voice( - "Compliance", - "You care about SOC2 Type II, ISO 27001, vendor maturity, and regulatory blast-radius.", -) - - -approver = Agent( - model="oci:openai.gpt-5.5", - tools=[submit_po, email_cfo], - system_prompt=( - "You are the Approval Officer. Given a recommended vendor and " - "amount, (1) call submit_po exactly once, (2) call email_cfo " - "exactly once with a one-paragraph summary as the body. Then " - "reply in one sentence: which PO was submitted and to whom the " - "email went." - ), - checkpointer=OCIBucketBackend(bucket_name=BUCKET, namespace=NAMESPACE, profile_name=PROFILE), - max_iterations=6, -) - - -# ─── Pretty print + streaming ────────────────────────────────────────────── -R = "\033[38;2;199;70;52m" # Oracle red -K = "\033[38;2;120;113;108m" -G = "\033[38;2;76;179;123m" -Y = "\033[38;2;255;180;84m" -P = "\033[38;2;200;168;255m" -B = "\033[1m" -Z = "\033[0m" - - -def _hr(char: str = "─") -> None: - print(char * 92) - - -def _section(title: str) -> None: - _hr() - print(f" {title}") - _hr() - - -def _emit_wrapped(text: str, label: str, color: str, width: int = 86) -> None: - """Print already-collected text wrapped, with the agent's label gutter.""" - indent = " " * (len(label) + 2) - paras = text.strip().split("\n") - first = True - for para in paras: - if not para.strip(): - print() - continue - for line in textwrap.wrap(para, width=width) or [""]: - if first: - print(f" {color}{label}\033[0m {line}") - first = False - else: - print(f" {color}{' ' * len(label)}\033[0m {line}") - - -async def _stream_agent(agent: Agent, prompt: str, role: str, color: str) -> str: - """Run the agent, then print the answer wrapped to a legible width. - - We collect tokens silently while the model streams, then render the final - text wrapped — much more readable on a 1500-px terminal than letting the - model wrap at whatever width OCI's V1 transport hands back. - """ - label = f"{role:<12}│" - streamed = "" - tools_fired: list[str] = [] - async for event in agent.run(prompt): - if isinstance(event, ModelChunkEvent) and event.content: - streamed += event.content - elif isinstance(event, ToolStartEvent): - tools_fired.append(event.tool_name) - elif isinstance(event, TerminateEvent): - final = event.final_message or streamed - for t in tools_fired: - print(f" {color}{' ' * len(label)}\033[0m \033[2m· tool: {t}\033[0m") - _emit_wrapped(final, label, color) - return final - _emit_wrapped(streamed, label, color) - return streamed - - -# ─── Slides ──────────────────────────────────────────────────────────────── -def _slide_pitch() -> None: - print("\033[2J\033[H") - print() - print(f" {B}What we're making{Z}") - print() - print(" Three Oracle GenAI agents that approve a $2M cloud-infrastructure PO.") - print() - print(f" {Y}🧾 Procurement{Z} queries Oracle 26ai vendor catalogue.") - print(f" {P}🛡 Compliance{Z} queries the same catalogue for SOC2 / ISO.") - print(f" {Y}🧾 Procurement{Z} ↔ {P}🛡 Compliance{Z} debate trade-offs, agree.") - print(f" {G}✍️ Approval Officer{Z} asks {B}you{Z} for consent, then submits + emails.") - print() - print(f" {K}Every line of agent text below is a real Oracle GenAI gpt-5.5 response.{Z}") - print() - time.sleep(5.0) - - -def _slide_outro() -> None: - print() - print(f" {B}{G}✓ PO approved by 3 agents · 1 human · zero duplicate submissions.{Z}") - print() - print(f" {K}powered by{Z} {B}{R}locus{Z} {K}on{Z} Oracle 26ai") - print() - - -# ─── Main ────────────────────────────────────────────────────────────────── -async def main() -> None: - # The Playwright-rendered intro + scenes (logo, problem, dashboard) are - # concatenated separately in the video pipeline. The terminal demo skips - # pitch and goes straight to the agentic execution. - _section("PREFLIGHT — live services") - with oracledb.connect( - user="ADMIN", - password=ORACLE_PW, - dsn=ORACLE_DSN, - config_dir=WALLET, - wallet_location=WALLET, - wallet_password=ORACLE_PW, - ) as conn: - cur = conn.cursor() - cur.execute("SELECT banner_full FROM v$version") - banner = cur.fetchone()[0].splitlines()[0] - cur.execute("SELECT count(*) FROM VENDOR_CATALOG") - rows = cur.fetchone()[0] - print(f" ✓ {banner}") - print(f" ✓ VENDOR_CATALOG — {rows} rows · VECTOR(1024, FLOAT32)") - print(" ✓ OCI GenAI us-chicago-1 · openai.gpt-5.5 + cohere.embed-english-v3.0") - print(f" ✓ OCI Object Storage · oci://{NAMESPACE}/{BUCKET}") - print() - - user_prompt = ( - "Approve a $2M cloud infrastructure spend for FY26. Recommend a vendor and submit the PO." - ) - _section("USER") - print(f" {user_prompt}") - print() - - # ── Round 1: Procurement ──────────────────────────────────────────── - _section("ROUND 1 · 🧾 Procurement queries Oracle 26ai") - proc_text = await _stream_agent( - procurement, "List your three vendor candidates.", "🧾 PROCUREMENT", Y - ) - - # ── Round 2: Compliance ───────────────────────────────────────────── - _section("ROUND 2 · 🛡 Compliance queries Oracle 26ai") - comp_text = await _stream_agent( - compliance, "List your three compliance picks.", "🛡 COMPLIANCE", P - ) - - # ── Round 3: Procurement reacts ───────────────────────────────────── - _section("ROUND 3 · 🧾 Procurement replies to 🛡 Compliance") - react_prompt = ( - f"Your vendor picks: {proc_text}\n\n" - f"🛡 Compliance just sent their picks: {comp_text}\n\n" - "Reply in 2-3 sentences (prose, no bullets). Do their compliance " - "picks fit our $2M cap? Name the trade-off and a recommendation." - ) - proc_reaction = await _stream_agent(procurement_voice, react_prompt, "🧾 PROCUREMENT", Y) - - # ── Round 4: Compliance replies ───────────────────────────────────── - _section("ROUND 4 · 🛡 Compliance replies to 🧾 Procurement") - counter_prompt = ( - f"Your picks were: {comp_text}\n\n" - f"🧾 Procurement just replied: {proc_reaction}\n\n" - "Reply in 2-3 sentences (prose). Agree with what makes sense, " - "push back if compliance is being undervalued. Reference vendor ids." - ) - comp_counter = await _stream_agent(compliance_voice, counter_prompt, "🛡 COMPLIANCE", P) - - # ── Round 5: Procurement writes the joint recommendation ──────────── - _section("ROUND 5 · 🧾 Procurement writes the joint recommendation") - plan_prompt = ( - f"Your picks: {proc_text}\n\n" - f"Compliance picks: {comp_text}\n\n" - f"Your reaction: {proc_reaction}\n\n" - f"Compliance response: {comp_counter}\n\n" - "Write the final recommendation as 2 short sentences: ONE vendor " - "id, the proposed annual amount in USD, the proposed payment term " - "in days, and one-line rationale. Use only ids that appeared above." - ) - plan_text = await _stream_agent(procurement, plan_prompt, "🧾 PROCUREMENT", Y) - - # ── Round 6a: human-in-the-loop consent ───────────────────────────── - _section("ROUND 6 · 👤 Human-in-the-loop · approve before submission") - print(" The next step will:") - print(" 1) submit_po — non-trivial: a real PO into the ledger") - print(" 2) email_cfo — to the CFO with the joint recommendation") - print() - answer = input(" Approve? [y/N] ").strip().lower() - if answer != "y": - print("\n ✗ Declined. No PO submitted, no email.") - return - print(" ✓ Approved.\n") - - # ── Round 6b: Approver fires the writes ───────────────────────────── - _section("ROUND 6 · ✍️ Approval Officer → submit + email") - handoff = ( - f"Procurement and Compliance agreed on:\n\n{plan_text}\n\n" - "Submit the PO to that vendor for the recommended amount and term, " - "exactly once. Then email_cfo at cfo@org.com with the joint " - "recommendation as the body." - ) - final_text = await _stream_agent(approver, handoff, "✍️ APPROVER", G) - print() - - # ── Verification ──────────────────────────────────────────────────── - _section("VERIFICATION") - print(f" 3 agents · 5 LLM rounds · 2 tool calls into Oracle 26ai") - print(f" submit_po body invocations: {len(_PO_SUBMITTED)} (idempotent — 1 even on retries)") - print(f" email_cfo body invocations: {len(_EMAILS)}") - if _EMAILS: - e = _EMAILS[-1] - print() - print(f" 📨 EMAIL · {e['to']} · subject: {e['subject']!r}") - print() - for line in textwrap.wrap(e["body"], width=82): - print(f" {line}") - _hr("═") - _slide_outro() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/po_approval/po_approval.tape b/examples/demos/po_approval/po_approval.tape deleted file mode 100644 index d263058..0000000 --- a/examples/demos/po_approval/po_approval.tape +++ /dev/null @@ -1,29 +0,0 @@ -# Locus — three agents collaborate on a vendor PO approval (real services) -# -# The Playwright-rendered intro + scenes (logo, problem, workflow) are -# concatenated separately. This tape captures only the live run. - -Output po_approval_run.gif -Set FontSize 14 -Set Width 1500 -Set Height 800 -Set Padding 22 -Set Theme "Catppuccin Mocha" -Set TypingSpeed 16ms -Set Shell "bash" - -Hide -Type "cd $(mktemp -d) && clear" Enter -Sleep 200ms -Show - -# Live run only — no nvim, no code walkthrough. -Type "python /Users/federico.kamelhar/Projects/locus/examples/demos/po_approval/po_approval.py" -Enter -Sleep 70000ms - -# Approval prompt — type y. -Type "y" -Enter - -Sleep 24000ms diff --git a/examples/demos/po_approval/setup_corpus.py b/examples/demos/po_approval/setup_corpus.py deleted file mode 100644 index 3f84d70..0000000 --- a/examples/demos/po_approval/setup_corpus.py +++ /dev/null @@ -1,106 +0,0 @@ -"""One-shot ingest of a vendor catalogue into Oracle 26ai. - -Eight vendor entries with pricing, certifications and payment terms. -Idempotent — re-running is a no-op once VENDOR_CATALOG is populated. -""" - -from __future__ import annotations - -import asyncio -import os - -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever - - -CORPUS = [ - ( - "vendor-acme-cloud", - "ACME Cloud Services. Compute + storage. Annual list $2.4M. " - "SOC2 Type II + ISO 27001. Payment: NET-60. " - "Used by 4 of the Fortune-500 banks. Strong incumbent.", - ), - ( - "vendor-techgrid-dc", - "TechGrid Datacenter. Colo + bare-metal. Annual list $1.8M. " - "SOC2 Type I only. Payment: NET-30. Solid mid-tier; " - "smaller blast radius than ACME but no Type II yet.", - ), - ( - "vendor-bytewave", - "ByteWave Storage. Object + cold-tier. Annual list $0.9M. " - "ISO 27001 only, no SOC2. Payment: NET-45. " - "Cheapest viable option but compliance gap on SOC2.", - ), - ( - "vendor-quantumstream", - "QuantumStream Networks. SD-WAN + private interconnect. " - "Annual list $3.1M. SOC2 + HIPAA + FedRAMP Moderate. " - "Payment: NET-90. Premium tier; strict compliance.", - ), - ( - "vendor-edgecdn", - "EdgeCDN Global. Edge delivery + DDoS. Annual list $0.62M. " - "ISO 27001. Payment: NET-30. Niche; not a primary cloud " - "provider, doesn't satisfy compute spend.", - ), - ( - "vendor-meridian", - "Meridian Cloud. Compute + database. Annual list $2.1M. " - "SOC2 Type II + HIPAA. Payment: NET-45. Comparable to " - "ACME on compliance, ~12% cheaper, smaller market share.", - ), - ( - "vendor-cobalt-labs", - "Cobalt Labs. AI-managed Kubernetes. Annual list $1.4M. " - "SOC2 Type II. Payment: NET-30. Strong technical fit, " - "but only 18 months old — vendor-risk concern.", - ), - ( - "vendor-orion-systems", - "Orion Systems. Bare-metal + GPU. Annual list $2.8M. " - "SOC2 + ISO 27001. Payment: NET-60. Heavy on GPU, light " - "on storage. Good if AI workloads dominate.", - ), -] - - -async def main() -> None: - profile = os.environ.get("OCI_PROFILE", "DEFAULT") - pw = os.environ["ORACLE_PASSWORD"] - wallet = os.environ.get("ORACLE_WALLET", os.path.expanduser("~/.oci/wallets/deepresearch")) - compartment = os.environ.get( - "OCI_COMPARTMENT", - "ocid1.tenancy.oc1..aaaaaaaaqlhpnytg33ztkwrdpq62p5yxx5gn5ltmkah23m7qebwjzc7x3lcq", - ) - - embedder = OCIEmbeddings( - model_id="cohere.embed-english-v3.0", - profile_name=profile, - compartment_id=compartment, - service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - ) - store = OracleVectorStore( - dsn=os.environ.get("ORACLE_DSN", "deepresearch_low"), - user="ADMIN", - password=pw, - wallet_location=wallet, - wallet_password=pw, - dimension=1024, - table_name="VENDOR_CATALOG", - ) - retriever = RAGRetriever(embedder=embedder, store=store) - - already = await retriever.retrieve("compute vendor", limit=1) - if already.documents: - print(f"VENDOR_CATALOG already populated. {len(CORPUS)} expected.") - return - - print(f"Ingesting {len(CORPUS)} vendor entries into Oracle 26ai…") - for doc_id, content in CORPUS: - await retriever.add_document(content, doc_id=doc_id, chunk=False) - print(f" + {doc_id}") - print("Done.") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/trip_team/.gitignore b/examples/demos/trip_team/.gitignore deleted file mode 100644 index 0464f68..0000000 --- a/examples/demos/trip_team/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Generated media — re-render with `python trip_team.py` + the .tape file. -*.mp4 -*.gif diff --git a/examples/demos/trip_team/setup_corpus.py b/examples/demos/trip_team/setup_corpus.py deleted file mode 100644 index bc290cd..0000000 --- a/examples/demos/trip_team/setup_corpus.py +++ /dev/null @@ -1,107 +0,0 @@ -"""One-shot ingest of the Tokyo trip corpus into Oracle 26ai. - -Two themes interleaved: food picks and culture picks. The cosine -distance over Cohere embeddings naturally separates them at search -time — each specialist gets its own query. -""" - -from __future__ import annotations - -import asyncio -import os - -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever - - -CORPUS = [ - # Food - ( - "afuri-shinjuku", - "Afuri Shinjuku — late-night yuzu shio ramen in Shinjuku. Light, " - "citrusy broth; vegetarian option. Open until 4am, queues short " - "after midnight.", - ), - ( - "tomoe-sushi", - "Tomoe Sushi — Edomae omakase in Hatchobori. Fifteen courses, " - "counter-only. Books out four weeks ahead; the hardest " - "reservation in this corpus.", - ), - ( - "donjaca-izakaya", - "Donjaca — standing izakaya in Shinbashi. No English menu, no " - "tourists. Famous for their potato salad. Quick stop, one drink, " - "move on.", - ), - ( - "uoshin-nogizaka", - "Uoshin Nogizaka — fish izakaya, sashimi delivered direct from " - "Tsukiji. Friendly to walk-ins. Good night-one warm-up.", - ), - # Culture - ( - "jbs-shibuya", - "JBS Shibuya — jazz listening bar tucked above a Family Mart. " - "Owner-curated vinyl, 9 pm onward, conversation discouraged. The " - "right cooldown after omakase.", - ), - ( - "blue-note-tokyo", - "Blue Note Tokyo — flagship jazz club in Roppongi. Two sets " - "nightly; book the second for a late-evening cap.", - ), - ( - "morioka-shoten", - "Morioka Shoten — one-book-a-week shop in Ginza. The proprietor " - "picks a single title and runs it for seven days. Obscure, " - "perfect for the brief.", - ), - ( - "jimbocho-passage", - "Jimbocho used-book passage — three blocks of secondhand " - "stores. You can lose an entire afternoon. Strong on fine art " - "monographs and out-of-print Japanese fiction.", - ), -] - - -async def main() -> None: - profile = os.environ.get("OCI_PROFILE", "DEFAULT") - pw = os.environ["ORACLE_PASSWORD"] - wallet = os.environ.get("ORACLE_WALLET", os.path.expanduser("~/.oci/wallets/deepresearch")) - compartment = os.environ.get( - "OCI_COMPARTMENT", - "ocid1.tenancy.oc1..aaaaaaaaqlhpnytg33ztkwrdpq62p5yxx5gn5ltmkah23m7qebwjzc7x3lcq", - ) - - embedder = OCIEmbeddings( - model_id="cohere.embed-english-v3.0", - profile_name=profile, - compartment_id=compartment, - service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - ) - store = OracleVectorStore( - dsn=os.environ.get("ORACLE_DSN", "deepresearch_low"), - user="ADMIN", - password=pw, - wallet_location=wallet, - wallet_password=pw, - dimension=1024, - table_name="TOKYO_TRIP_RECS", - ) - retriever = RAGRetriever(embedder=embedder, store=store) - - already = await retriever.retrieve("ramen", limit=1) - if already.documents: - print(f"TOKYO_TRIP_RECS already populated. {len(CORPUS)} expected.") - return - - print(f"Ingesting {len(CORPUS)} Tokyo recommendations into Oracle 26ai…") - for doc_id, content in CORPUS: - await retriever.add_document(content, doc_id=doc_id, chunk=False) - print(f" + {doc_id}") - print("Done.") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/trip_team/trip_team.py b/examples/demos/trip_team/trip_team.py deleted file mode 100644 index 4d94961..0000000 --- a/examples/demos/trip_team/trip_team.py +++ /dev/null @@ -1,439 +0,0 @@ -# ruff: noqa: ASYNC250, F841, ASYNC221, S603, S607, RUF001 -"""Three locus agents collaborate to plan a Tokyo trip — real, runnable. - -Pipeline (each step is a real ``Agent.run_sync`` against OCI GenAI): - - 1. Foodie — searches Oracle 26ai for restaurants. Real RAG. - 2. Culture — searches Oracle 26ai for jazz / bookstores. Real RAG. - 3. Foodie — receives Culture's picks, produces a joint 3-day plan. - This is the "two agents agree" beat. - 4. Concierge — receives the agreed plan, calls - @tool(idempotent=True) book_restaurant, then - @tool(idempotent=True) email_itinerary. Checkpointed - to OCI Object Storage on every iteration. - -Required env: - OCI_PROFILE (default DEFAULT) - ORACLE_PASSWORD (required) - ORACLE_WALLET (default ~/.oci/wallets/deepresearch) - OCI_NAMESPACE (required — for the OCI bucket checkpointer) - OCI_BUCKET_NAME (default locus-test-checkpoints) -""" - -from __future__ import annotations - -import asyncio -import os -import sys -import textwrap -import time -import uuid - -import oracledb - -from locus import Agent -from locus.core.events import ( - ModelChunkEvent, - TerminateEvent, - ToolCompleteEvent, - ToolStartEvent, -) -from locus.memory.backends.oci_bucket import OCIBucketBackend -from locus.rag import OCIEmbeddings, OracleVectorStore, RAGRetriever -from locus.tools.decorator import tool - - -# ─── Config ──────────────────────────────────────────────────────────────── -PROFILE = os.environ.get("OCI_PROFILE", "DEFAULT") -ORACLE_PW = os.environ["ORACLE_PASSWORD"] -WALLET = os.environ.get("ORACLE_WALLET", os.path.expanduser("~/.oci/wallets/deepresearch")) -ORACLE_DSN = os.environ.get("ORACLE_DSN", "deepresearch_low") -BUCKET = os.environ.get("OCI_BUCKET_NAME", "locus-test-checkpoints") -NAMESPACE = os.environ["OCI_NAMESPACE"] -COMPARTMENT = os.environ.get( - "OCI_COMPARTMENT", - "ocid1.tenancy.oc1..aaaaaaaaqlhpnytg33ztkwrdpq62p5yxx5gn5ltmkah23m7qebwjzc7x3lcq", -) - - -# ─── Retriever factory (fresh per call — async pools can't cross loops) ─── -def _new_retriever() -> RAGRetriever: - return RAGRetriever( - embedder=OCIEmbeddings( - model_id="cohere.embed-english-v3.0", - profile_name=PROFILE, - compartment_id=COMPARTMENT, - service_endpoint="https://inference.generativeai.us-chicago-1.oci.oraclecloud.com", - ), - store=OracleVectorStore( - dsn=ORACLE_DSN, - user="ADMIN", - password=ORACLE_PW, - wallet_location=WALLET, - wallet_password=ORACLE_PW, - dimension=1024, - table_name="TOKYO_TRIP_RECS", - ), - ) - - -# ─── Tools ───────────────────────────────────────────────────────────────── -@tool -def search_food(query: str, limit: int = 4) -> list[dict]: - """Search Oracle 26ai for Tokyo restaurants matching a theme.""" - retriever = _new_retriever() - rs = asyncio.run(retriever.retrieve(f"restaurant {query}", limit=limit)) - return [ - {"id": r.document.id, "content": r.document.content, "score": round(r.score, 3)} - for r in rs.documents - ] - - -@tool -def search_culture(query: str, limit: int = 4) -> list[dict]: - """Search Oracle 26ai for Tokyo jazz bars and bookstores.""" - retriever = _new_retriever() - rs = asyncio.run(retriever.retrieve(f"jazz bar bookstore {query}", limit=limit)) - return [ - {"id": r.document.id, "content": r.document.content, "score": round(r.score, 3)} - for r in rs.documents - ] - - -_BOOKED: dict[tuple[str, str], dict] = {} -_EMAILS: list[dict] = [] - - -@tool(idempotent=True) -def book_restaurant(name: str, when: str) -> dict: - """Book a restaurant. Idempotent — re-fires return the cached receipt.""" - key = (name, when) - if key in _BOOKED: - return {**_BOOKED[key], "cached": True} - receipt = { - "status": "booked", - "name": name, - "when": when, - "res_id": f"R-{abs(hash(key)) % 100000:05d}", - } - _BOOKED[key] = receipt - return receipt - - -@tool(idempotent=True) -def email_itinerary(to: str, subject: str, body: str) -> dict: - """Send the itinerary email. Idempotent.""" - _EMAILS.append({"to": to, "subject": subject, "body": body}) - return {"status": "sent", "to": to, "chars": len(body)} - - -# ─── Three agents ────────────────────────────────────────────────────────── -foodie = Agent( - model="oci:openai.gpt-5.5", - tools=[search_food], - system_prompt=( - "You are the Foodie agent on a Tokyo trip-planning team. " - "Call search_food EXACTLY ONCE with a broad query like " - "'ramen omakase izakaya'. Then immediately stop calling tools " - "and write your three picks ONLY using ids that appeared in the " - "results: one late-night ramen for day 1, one hard-to-book " - "omakase for day 2, one quick izakaya stop. Format: 3 bullets " - "— name, id, one-line reasoning each. Be terse. Do not invent " - "names that weren't in the search results." - ), - max_iterations=3, -) - -culture = Agent( - model="oci:openai.gpt-5.5", - tools=[search_culture], - system_prompt=( - "You are the Culture agent. Step 1: call search_culture(query='jazz'). " - "Step 2: read the list of {id, content, score} entries the tool returned. " - "Step 3: list THREE of those entries verbatim, one per line, in this format:\n" - " - \n" - "Pick: a jazz cooldown after omakase, a bigger jazz set for day 3, " - "an obscure bookstore. Use ONLY ids from the tool result. Stop after writing." - ), - max_iterations=5, -) - - -def _make_voice(name: str, personality: str) -> Agent: - """Free-form persona for the dialogue rounds — no tools, no bullets.""" - return Agent( - model="oci:openai.gpt-5.5", - system_prompt=( - f"You are {name}, a member of a Tokyo trip-planning team. " - f"{personality} " - "You are in a CONVERSATION with another team member — not " - "answering a request. Reply in 2-3 sentences of plain prose. " - "No bullets. No formatted lists. Reference specific ids when " - "useful, but write like a person, not a report." - ), - max_iterations=2, - ) - - -foodie_voice = _make_voice( - "🍜 Foodie", - "You care about food timing, queues, and reservation difficulty.", -) -culture_voice = _make_voice( - "🎷 Culture", - "You care about late-night listening rooms, vinyl, obscure bookstores.", -) - - -concierge = Agent( - model="oci:openai.gpt-5.5", - tools=[book_restaurant, email_itinerary], - system_prompt=( - "You are the Concierge. Given an agreed Tokyo itinerary, " - "(1) call book_restaurant once for the omakase reservation, " - "(2) call email_itinerary once with the full itinerary as the " - "body. Then reply in one sentence: what you booked + emailed." - ), - checkpointer=OCIBucketBackend(bucket_name=BUCKET, namespace=NAMESPACE, profile_name=PROFILE), - max_iterations=6, -) - - -# ─── Pretty print helpers ────────────────────────────────────────────────── -def _hr(char: str = "─") -> None: - print(char * 92) - - -def _section(title: str) -> None: - _hr() - print(f" {title}") - _hr() - - -def _agent_line(role: str, color: str, text: str) -> None: - width = 78 - label = f"{role:<10}│" - lines = textwrap.wrap(text.strip(), width=width) or [""] - print(f" \033[{color}m{label}\033[0m {lines[0]}") - for line in lines[1:]: - print(f" \033[{color}m{' ' * len(label)}\033[0m {line}") - - -async def _stream_agent(agent: Agent, prompt: str, role: str, color: str) -> str: - """Stream the agent's text token-by-token. Falls back to terminate.message - when the upstream model emits no streaming chunks (some models batch - the response and arrive only as TerminateEvent).""" - label = f"{role:<10}│" - print(f" {color}{label}\033[0m ", end="", flush=True) - streamed = "" - async for event in agent.run(prompt): - if isinstance(event, ModelChunkEvent) and event.content: - sys.stdout.write(event.content) - sys.stdout.flush() - streamed += event.content - elif isinstance(event, ToolStartEvent): - sys.stdout.write( - f"\n {color}{' ' * len(label)}\033[0m \033[2m· tool: {event.tool_name}\033[0m\n" - ) - sys.stdout.write(f" {color}{label}\033[0m ") - sys.stdout.flush() - elif isinstance(event, TerminateEvent): - final = event.final_message or streamed - # Print whatever wasn't streamed yet, slowly enough to feel live. - tail = final[len(streamed) :] - for ch in tail: - sys.stdout.write(ch) - sys.stdout.flush() - await asyncio.sleep(0.005) - print() - return final - return streamed - - -# Truecolor — exact hexes from docs/img/logo.svg. -R = "\033[38;2;199;70;52m" # Oracle red #C74634 -K = "\033[38;2;120;113;108m" # tagline gray #78716C -D = "\033[38;2;168;162;156m" # dim gray #A8A29E -G = "\033[38;2;76;179;123m" # green #4CB37B -Y = "\033[38;2;255;180;84m" # yellow #FFB454 -P = "\033[38;2;200;168;255m" # purple #C8A8FF -B = "\033[1m" -Z = "\033[0m" - - -def _slide_intro() -> None: - """Hold on the logo long enough for it to register.""" - print("\n\n\n") - print(f" {K}╲ ╱{Z}") - print(f" {K}╲ ╱{Z}") - print(f" {R}┌─{K}╲ ╱{R}─┐{Z}") - print(f" {R}│ {R}█{R} │{Z}") - print(f" {R}└─{K}╱ ╲{R}─┘{Z}") - print(f" {K}╱ ╲{Z}") - print(f" {K}╱ ╲{Z}") - print() - print(f" {B}locus{Z}") - print(f" {K}ORACLE GENERATIVE AI · MULTI-AGENT ORCHESTRATOR SDK{Z}") - print() - print() - print(f" {K}github.com/oracle/locus · examples/demos/trip_team/{Z}") - print("\n\n") - time.sleep(7.0) - - -def _slide_pitch() -> None: - """What we're making.""" - print("\033[2J\033[H") - print() - print(f" {B}What we're making{Z}") - print() - print(" Three Oracle GenAI agents that talk to each other to plan a 3-day Tokyo trip.") - print() - print(f" {Y}🍜 Foodie{Z} retrieves restaurants from Oracle 26ai.") - print(f" {P}🎷 Culture{Z} retrieves jazz bars and bookstores from Oracle 26ai.") - print( - f" {Y}🍜 Foodie{Z} ↔ {P}🎷 Culture{Z} debate picks, respond to each other, agree." - ) - print(f" {G}🛎️ Concierge{Z} asks {B}you{Z} for approval, then books + emails.") - print() - print(f" {K}Every line of agent text below is a real Oracle GenAI gpt-5.5 response.{Z}") - print() - time.sleep(5.0) - - -def _slide_outro() -> None: - print() - print(f" {B}{G}✓ Three agents · one trip · human-approved · zero double-charges.{Z}") - print() - print(f" {K}powered by{Z} {B}{R}locus{Z} {K}on{Z} Oracle 26ai") - print() - - -async def main() -> None: - # Intro slide is rendered as a separate Playwright video, then - # ffmpeg-concatenated to this run. We start straight at the pitch. - _slide_pitch() - print("\033[2J\033[H") # clear before the live run starts - - _section("PREFLIGHT — live services") - with oracledb.connect( - user="ADMIN", - password=ORACLE_PW, - dsn=ORACLE_DSN, - config_dir=WALLET, - wallet_location=WALLET, - wallet_password=ORACLE_PW, - ) as conn: - cur = conn.cursor() - cur.execute("SELECT banner_full FROM v$version") - banner = cur.fetchone()[0].splitlines()[0] - cur.execute("SELECT count(*) FROM TOKYO_TRIP_RECS") - rows = cur.fetchone()[0] - print(f" ✓ {banner}") - print(f" ✓ TOKYO_TRIP_RECS — {rows} rows · VECTOR(1024, FLOAT32)") - print(" ✓ OCI GenAI us-chicago-1 · openai.gpt-5.5 + cohere.embed-english-v3.0") - print(f" ✓ OCI Object Storage · oci://{NAMESPACE}/{BUCKET}") - print() - - user_prompt = ( - "Plan a 3-day Tokyo trip around food, jazz, and bookstores. " - "Book what's hard. Email me at me@org.com." - ) - _section("USER") - print(f" {user_prompt}") - print() - - # ── Round 1: Foodie picks (streaming) ────────────────────────────── - _section("ROUND 1 · 🍜 Foodie searches Oracle 26ai and proposes") - foodie_text = await _stream_agent( - foodie, "Pick the food spots. Be terse.", "🍜 FOODIE", "\033[38;2;255;180;84m" - ) - - # ── Round 2: Culture picks (streaming) ───────────────────────────── - _section("ROUND 2 · 🎷 Culture searches Oracle 26ai and proposes") - culture_text = await _stream_agent( - culture, "Pick the culture spots. Be terse.", "🎷 CULTURE", "\033[38;2;200;168;255m" - ) - - # ── Round 3: Foodie reacts to Culture's picks (streaming) ────────── - _section("ROUND 3 · 🍜 Foodie replies to 🎷 Culture") - react_prompt = ( - f"Your food picks were: {foodie_text}\n\n" - f"🎷 Culture just sent you their picks: {culture_text}\n\n" - "Reply to Culture in 2-3 sentences (prose, no bullets) about how " - "their picks fit your food schedule. Mention specifically whether " - "jbs-shibuya works as a cooldown after the omakase, and name one " - "timing trade-off." - ) - foodie_reaction_text = await _stream_agent( - foodie_voice, react_prompt, "🍜 FOODIE", "\033[38;2;255;180;84m" - ) - - # ── Round 4: Culture replies (streaming) ─────────────────────────── - _section("ROUND 4 · 🎷 Culture replies to 🍜 Foodie") - counter_prompt = ( - f"Your culture picks were: {culture_text}\n\n" - f"🍜 Foodie just replied: {foodie_reaction_text}\n\n" - "Reply to Foodie in 2-3 sentences (prose, no bullets). Agree where " - "you can; push back if their timing concern is off. Reference at " - "least one id." - ) - culture_counter_text = await _stream_agent( - culture_voice, counter_prompt, "🎷 CULTURE", "\033[38;2;200;168;255m" - ) - - # ── Round 5: Foodie writes the agreed joint plan (streaming) ─────── - _section("ROUND 5 · 🍜 Foodie writes the agreed 3-day plan") - plan_prompt = ( - "You and 🎷 Culture have now agreed. Here's the conversation:\n\n" - f"Your picks:\n{foodie_text}\n\n" - f"Culture's picks:\n{culture_text}\n\n" - f"Your reaction:\n{foodie_reaction_text}\n\n" - f"Culture's response:\n{culture_counter_text}\n\n" - "Now write the final 3-day plan. Day-by-day, one line per slot. " - "Use ONLY ids that appeared above. Day 2 must have omakase right " - "before a jazz cooldown." - ) - plan_text = await _stream_agent(foodie, plan_prompt, "🍜 FOODIE", "\033[38;2;255;180;84m") - - # ── Round 6a: human-in-the-loop consent ───────────────────────────── - _section("ROUND 6 · 👤 Human-in-the-loop · approve before concierge fires") - print(" The next step will: 1) book_restaurant(Tomoe Sushi, 2026-05-09 19:30)") - print(" 2) email_itinerary → me@org.com") - print() - answer = input(" Approve? [y/N] ").strip().lower() - if answer != "y": - print("\n ✗ Declined. Concierge not invoked. No booking, no email.") - return - print(" ✓ Approved.\n") - - # ── Round 6b: Concierge handoff ───────────────────────────────────── - _section("ROUND 6 · 🛎️ Concierge → book + email") - handoff = ( - "The Foodie and Culture agents agreed on this 3-day Tokyo plan:\n\n" - f"{plan_text}\n\n" - "Book the omakase at Tomoe Sushi for 2026-05-09 19:30 using " - "book_restaurant exactly once. Then email_itinerary the full plan " - "to me@org.com exactly once." - ) - final_text = await _stream_agent(concierge, handoff, "🛎️ CONCIERGE", "\033[38;2;76;179;123m") - print() - - # ── Verification ──────────────────────────────────────────────────── - _section("VERIFICATION") - print(f" 3 agents · 4 LLM rounds · 2 tool calls into Oracle 26ai") - print(f" book_restaurant body invocations: {len(_BOOKED)} (idempotent — 1 even on retries)") - print(f" email_itinerary body invocations: {len(_EMAILS)}") - if _EMAILS: - e = _EMAILS[-1] - print() - print(f" 📨 EMAIL · {e['to']} · subject: {e['subject']!r}") - print() - for line in textwrap.wrap(e["body"], width=82): - print(f" {line}") - _hr("═") - _slide_outro() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/demos/trip_team/trip_team.tape b/examples/demos/trip_team/trip_team.tape deleted file mode 100644 index 52c816a..0000000 --- a/examples/demos/trip_team/trip_team.tape +++ /dev/null @@ -1,48 +0,0 @@ -# Locus — three agents collaborate on a Tokyo trip plan (real services) -# -# The Playwright-rendered intro + scenes (logo + problem + architecture) -# are concatenated separately. This tape captures only the terminal: -# 1. nvim — the program first. -# 2. python — the live run with streaming + human consent. - -Output trip_team_run.gif -Set FontSize 14 -Set Width 1500 -Set Height 800 -Set Padding 22 -Set Theme "Catppuccin Mocha" -Set TypingSpeed 16ms -Set Shell "bash" - -Hide -Type "cd $(mktemp -d) && clear" Enter -Sleep 200ms -Show - -# --- Code reveal in nvim -------------------------------------------------- -Type `nvim -R -c "set hlsearch" -c "let @/='@tool\|idempotent\|input(\|Agent(\|search_food\|search_culture\|book_restaurant\|email_itinerary'" /Users/federico.kamelhar/Projects/locus/examples/demos/trip_team/trip_team.py` -Enter -Sleep 3500ms -PageDown -Sleep 7000ms -PageDown -Sleep 7000ms -PageDown -Sleep 7000ms -Type ":q" -Enter -Sleep 400ms -Type "clear" -Enter -Sleep 200ms - -# --- Live run ------------------------------------------------------------- -Type "python /Users/federico.kamelhar/Projects/locus/examples/demos/trip_team/trip_team.py" -Enter -Sleep 70000ms - -# Approval prompt — type y. -Type "y" -Enter - -Sleep 24000ms