feat: add OpenAI Agents SDK cookbook example#146
feat: add OpenAI Agents SDK cookbook example#146desiorac wants to merge 2 commits intousemoss:mainfrom
Conversation
Add cookbook example showing how to use Moss semantic search with the OpenAI Agents SDK. Includes function tools for search (with optional metadata filters), document management, and index listing. Demo: multi-agent travel planner where specialist agents search separate Moss indexes and a planner orchestrates via agent.as_tool(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
| if not result.docs: | ||
| return "No documents found." | ||
| return "\n\n".join( | ||
| f"[{doc.id}]: {doc.text[:200]}{'...' if len(doc.text) > 200 else ''}" | ||
| for doc in result.docs | ||
| ) |
There was a problem hiding this comment.
🔴 get_docs returns List[DocumentInfo], not an object with .docs — causes AttributeError
MossClient.get_docs() returns List[DocumentInfo] (confirmed by the type stub at sdks/python/sdk/src/moss/__init__.pyi:41-45, tests at sdks/python/sdk/tests/test_client.py:260-276, and the e2e test at sdks/python/sdk/tests/test_e2e.py:284-288). However, moss_get_docs_tool treats the return value as an object with a .docs attribute (like SearchResult), accessing result.docs on lines 146 and 150. This will raise AttributeError: 'list' object has no attribute 'docs' every time the tool is invoked. The existing crewai cookbook (examples/cookbook/crewai/moss_crewai.py:168-172) correctly iterates over the list directly.
| if not result.docs: | |
| return "No documents found." | |
| return "\n\n".join( | |
| f"[{doc.id}]: {doc.text[:200]}{'...' if len(doc.text) > 200 else ''}" | |
| for doc in result.docs | |
| ) | |
| if not result: | |
| return "No documents found." | |
| return "\n\n".join( | |
| f"[{doc.id}]: {doc.text[:200]}{'...' if len(doc.text) > 200 else ''}" | |
| for doc in result | |
| ) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| Args: | ||
| doc_ids: Optional list of document IDs to retrieve. | ||
| """ | ||
| options = GetDocumentsOptions(ids=doc_ids) if doc_ids else GetDocumentsOptions() |
There was a problem hiding this comment.
🔴 Wrong keyword argument ids instead of doc_ids in GetDocumentsOptions
GetDocumentsOptions.__init__ accepts doc_ids as its parameter (per sdks/python/sdk/src/moss/__init__.pyi:85), but the tool passes ids=doc_ids. Every other usage in the codebase uses doc_ids= — see sdks/python/sdk/tests/test_client.py:264, examples/cookbook/crewai/moss_crewai.py:167, examples/python/comprehensive_sample.py:277. Since GetDocumentsOptions is a Rust-backed PyO3 class, passing an unknown keyword argument ids will either raise a TypeError or silently create an empty options object that fetches all documents instead of the requested subset.
| options = GetDocumentsOptions(ids=doc_ids) if doc_ids else GetDocumentsOptions() | |
| options = GetDocumentsOptions(doc_ids=doc_ids) if doc_ids else GetDocumentsOptions() |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Pull request overview
Adds a new cookbook example demonstrating how to use Moss semantic search as tools within the OpenAI Agents SDK, including a multi-agent demo and live tests.
Changes:
- Introduces
moss_openai_agents.pytool factory functions wrappingMossClientoperations for OpenAI Agents SDK. - Adds an interactive multi-agent travel planner demo (
example_usage.py) plus travel dataset JSON files. - Adds setup/docs (
README.md,.env.example) and a live-test script (test_live.py) for platform validation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/cookbook/openai-agents/moss_openai_agents.py | Implements Agents SDK function tools for Moss search, filtering, CRUD, and index listing. |
| examples/cookbook/openai-agents/example_usage.py | Multi-agent travel planner demo wiring specialist agents + planner orchestration. |
| examples/cookbook/openai-agents/test_live.py | Scripted “live” verification of tool operations against the Moss API. |
| examples/cookbook/openai-agents/README.md | Cookbook documentation: setup, quick start, tool descriptions, and demo instructions. |
| examples/cookbook/openai-agents/pyproject.toml | Declares the example as a Python project with dependencies. |
| examples/cookbook/openai-agents/.env.example | Environment variable template for Moss/OpenAI credentials. |
| examples/cookbook/openai-agents/data/destinations_moss.json | Travel destination documents used by the demo. |
| examples/cookbook/openai-agents/data/stays_moss.json | Travel stays documents used by the demo. |
| examples/cookbook/openai-agents/data/activities_moss.json | Travel activities documents used by the demo. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Args: | ||
| doc_ids: Optional list of document IDs to retrieve. | ||
| """ | ||
| options = GetDocumentsOptions(ids=doc_ids) if doc_ids else GetDocumentsOptions() |
There was a problem hiding this comment.
GetDocumentsOptions is constructed with ids=..., but the Moss SDK uses doc_ids (see other examples/tests). As written, this will raise a TypeError at runtime and break moss_get_docs.
| options = GetDocumentsOptions(ids=doc_ids) if doc_ids else GetDocumentsOptions() | |
| options = GetDocumentsOptions(doc_ids=doc_ids) if doc_ids else GetDocumentsOptions() |
| opts = QueryOptions( | ||
| top_k=top_k, | ||
| alpha=alpha, | ||
| filter={filter_field: filter_value}, |
There was a problem hiding this comment.
The metadata filter passed to QueryOptions is not in the filter expression format used by Moss (it expects objects like { "field": ..., "condition": {"$eq": ...} } and logical operators such as $and). Passing {filter_field: filter_value} is likely to be ignored or error in local queries; build a proper $eq filter expression instead.
| filter={filter_field: filter_value}, | |
| filter={"field": filter_field, "condition": {"$eq": filter_value}}, |
| ids: Optional document IDs (auto-generated if omitted). | ||
| upsert: If True, update existing documents with the same ID. | ||
| """ | ||
| doc_ids = ids or [str(uuid.uuid4()) for _ in texts] |
There was a problem hiding this comment.
zip(doc_ids, texts) truncates to the shorter list. If ids is provided with a different length than texts, some documents will be silently dropped or some IDs ignored. Validate that len(ids) == len(texts) (or generate missing IDs per text, as other cookbooks do) before building docs.
| doc_ids = ids or [str(uuid.uuid4()) for _ in texts] | |
| if ids is None: | |
| doc_ids = [str(uuid.uuid4()) for _ in texts] | |
| else: | |
| if len(ids) != len(texts): | |
| raise ValueError("ids must have the same length as texts.") | |
| doc_ids = ids |
| nonlocal _index_loaded | ||
| if not _index_loaded: | ||
| await client.load_index(index_name) | ||
| _index_loaded = True | ||
|
|
There was a problem hiding this comment.
The lazy load_index() guard uses a plain boolean. If the tool can be invoked concurrently, multiple calls may race and trigger multiple load_index() calls. Consider protecting the first-load path with an asyncio.Lock/asyncio.Event (or memoizing the load task) so only one load happens per tool instance.
| import asyncio | ||
| import uuid | ||
| from typing import Any | ||
|
|
||
| from agents import RunContextWrapper, function_tool |
There was a problem hiding this comment.
Unused imports (asyncio, Any, RunContextWrapper) add noise and may fail linting in stricter environments. Remove them, or use RunContextWrapper in tool signatures if intended.
| import asyncio | |
| import uuid | |
| from typing import Any | |
| from agents import RunContextWrapper, function_tool | |
| import uuid | |
| from agents import function_tool |
| "openai-agents>=0.0.7", | ||
| "moss>=1.0.0", | ||
| "python-dotenv", | ||
| ] |
There was a problem hiding this comment.
This pyproject.toml uses Hatchling but does not declare what to include in the wheel/sdist (unlike other cookbook examples). With only flat modules (no package dir), pip install -e . / building a wheel is likely to omit moss_openai_agents.py and example_usage.py, causing ModuleNotFoundError. Add [tool.hatch.build.targets.wheel] packages = ["moss_openai_agents.py"] and include needed files under [tool.hatch.build.targets.sdist].
| ] | |
| ] | |
| [tool.hatch.build.targets.wheel] | |
| packages = ["moss_openai_agents.py"] | |
| [tool.hatch.build.targets.sdist] | |
| include = [ | |
| "moss_openai_agents.py", | |
| "example_usage.py", | |
| "README.md", | |
| ] |
| "text": "Nine Hours Capsule Hotel - Minimalist capsule stay in Shinjuku. High-speed Wi-Fi, lockers, and showers included. Price: \u00c2\u00a53,500 per night. Location: Shinjuku/Kanda. Amenities: Wi-Fi, Locker, Shower.", | ||
| "metadata": { | ||
| "location": "Shinjuku/Kanda", | ||
| "price": "\u00c2\u00a53,500" | ||
| } | ||
| }, | ||
| { | ||
| "id": "stay-tok-102", | ||
| "text": "Khaosan Tokyo Origami - Highly rated hostel in Asakusa. Offers dormitory beds and a communal kitchen to save on meal costs. Price: \u00c2\u00a52,800 per night. Location: Asakusa. Amenities: Kitchen, Laundry, Lounge.", | ||
| "metadata": { | ||
| "location": "Asakusa", | ||
| "price": "\u00c2\u00a52,800" | ||
| } | ||
| }, | ||
| { | ||
| "id": "stay-tok-103", | ||
| "text": "Sakura Hotel Ikebukuro - Budget hotel with private rooms and 24-hour cafe. Good for groups of two on a budget. Price: \u00c2\u00a57,000 per night. Location: Ikebukuro. Amenities: Private Room, Cafe, Multilingual Staff.", | ||
| "metadata": { | ||
| "location": "Ikebukuro", | ||
| "price": "\u00c2\u00a57,000" | ||
| } | ||
| }, | ||
| { | ||
| "id": "stay-vn-104", | ||
| "text": "Little Hanoi Hostel - Authentic hostel in the heart of the Old Quarter. Offers free breakfast and walking tours. Price: \u00c2\u00a51,200 per night. Location: Hanoi, Vietnam. Amenities: Free Breakfast, Walking Tours, Bicycle Rental.", | ||
| "metadata": { | ||
| "location": "Hanoi, Vietnam", | ||
| "price": "\u00c2\u00a51,200" | ||
| } | ||
| }, | ||
| { | ||
| "id": "stay-al-105", | ||
| "text": "Santi Quanta Hostel - Boutique hostel near the Skanderbeg Square. Known for a social atmosphere and clean facilities. Price: \u00c2\u00a52,000 per night. Location: Tirana, Albania. Amenities: Common Room, City Maps, Air Conditioning.", | ||
| "metadata": { | ||
| "location": "Tirana, Albania", | ||
| "price": "\u00c2\u00a52,000" | ||
| } | ||
| }, | ||
| { | ||
| "id": "stay-tr-106", | ||
| "text": "Cheers Lighthouse - Budget-friendly guesthouse in Sultanahmet with views of the Marmara Sea. Very close to the Blue Mosque. Price: \u00c2\u00a54,500 per night. Location: Istanbul, Turkey. Amenities: Ocean View, Restaurant, Airport Shuttle.", | ||
| "metadata": { | ||
| "location": "Istanbul, Turkey", | ||
| "price": "\u00c2\u00a54,500" | ||
| } | ||
| }, | ||
| { | ||
| "id": "stay-ro-107", | ||
| "text": "Kismet Dao Hostel - Legendary hostel in Brasov. Famous for its backyard BBQ and proximity to the Black Church. Price: \u00c2\u00a52,500 per night. Location: Brasov, Romania. Amenities: Garden BBQ, Lockers, Kitchen Access.", | ||
| "metadata": { | ||
| "location": "Brasov, Romania", | ||
| "price": "\u00c2\u00a52,500" |
There was a problem hiding this comment.
The travel data contains mojibake (e.g., \u00c2\u00a5 rendering as Â¥), which will show up in demo outputs and tool results. Re-encode these currency symbols/ranges to proper UTF-8 (e.g., ¥ and –) so users don’t see corrupted text.
| "text": "Nine Hours Capsule Hotel - Minimalist capsule stay in Shinjuku. High-speed Wi-Fi, lockers, and showers included. Price: \u00c2\u00a53,500 per night. Location: Shinjuku/Kanda. Amenities: Wi-Fi, Locker, Shower.", | |
| "metadata": { | |
| "location": "Shinjuku/Kanda", | |
| "price": "\u00c2\u00a53,500" | |
| } | |
| }, | |
| { | |
| "id": "stay-tok-102", | |
| "text": "Khaosan Tokyo Origami - Highly rated hostel in Asakusa. Offers dormitory beds and a communal kitchen to save on meal costs. Price: \u00c2\u00a52,800 per night. Location: Asakusa. Amenities: Kitchen, Laundry, Lounge.", | |
| "metadata": { | |
| "location": "Asakusa", | |
| "price": "\u00c2\u00a52,800" | |
| } | |
| }, | |
| { | |
| "id": "stay-tok-103", | |
| "text": "Sakura Hotel Ikebukuro - Budget hotel with private rooms and 24-hour cafe. Good for groups of two on a budget. Price: \u00c2\u00a57,000 per night. Location: Ikebukuro. Amenities: Private Room, Cafe, Multilingual Staff.", | |
| "metadata": { | |
| "location": "Ikebukuro", | |
| "price": "\u00c2\u00a57,000" | |
| } | |
| }, | |
| { | |
| "id": "stay-vn-104", | |
| "text": "Little Hanoi Hostel - Authentic hostel in the heart of the Old Quarter. Offers free breakfast and walking tours. Price: \u00c2\u00a51,200 per night. Location: Hanoi, Vietnam. Amenities: Free Breakfast, Walking Tours, Bicycle Rental.", | |
| "metadata": { | |
| "location": "Hanoi, Vietnam", | |
| "price": "\u00c2\u00a51,200" | |
| } | |
| }, | |
| { | |
| "id": "stay-al-105", | |
| "text": "Santi Quanta Hostel - Boutique hostel near the Skanderbeg Square. Known for a social atmosphere and clean facilities. Price: \u00c2\u00a52,000 per night. Location: Tirana, Albania. Amenities: Common Room, City Maps, Air Conditioning.", | |
| "metadata": { | |
| "location": "Tirana, Albania", | |
| "price": "\u00c2\u00a52,000" | |
| } | |
| }, | |
| { | |
| "id": "stay-tr-106", | |
| "text": "Cheers Lighthouse - Budget-friendly guesthouse in Sultanahmet with views of the Marmara Sea. Very close to the Blue Mosque. Price: \u00c2\u00a54,500 per night. Location: Istanbul, Turkey. Amenities: Ocean View, Restaurant, Airport Shuttle.", | |
| "metadata": { | |
| "location": "Istanbul, Turkey", | |
| "price": "\u00c2\u00a54,500" | |
| } | |
| }, | |
| { | |
| "id": "stay-ro-107", | |
| "text": "Kismet Dao Hostel - Legendary hostel in Brasov. Famous for its backyard BBQ and proximity to the Black Church. Price: \u00c2\u00a52,500 per night. Location: Brasov, Romania. Amenities: Garden BBQ, Lockers, Kitchen Access.", | |
| "metadata": { | |
| "location": "Brasov, Romania", | |
| "price": "\u00c2\u00a52,500" | |
| "text": "Nine Hours Capsule Hotel - Minimalist capsule stay in Shinjuku. High-speed Wi-Fi, lockers, and showers included. Price: ¥3,500 per night. Location: Shinjuku/Kanda. Amenities: Wi-Fi, Locker, Shower.", | |
| "metadata": { | |
| "location": "Shinjuku/Kanda", | |
| "price": "¥3,500" | |
| } | |
| }, | |
| { | |
| "id": "stay-tok-102", | |
| "text": "Khaosan Tokyo Origami - Highly rated hostel in Asakusa. Offers dormitory beds and a communal kitchen to save on meal costs. Price: ¥2,800 per night. Location: Asakusa. Amenities: Kitchen, Laundry, Lounge.", | |
| "metadata": { | |
| "location": "Asakusa", | |
| "price": "¥2,800" | |
| } | |
| }, | |
| { | |
| "id": "stay-tok-103", | |
| "text": "Sakura Hotel Ikebukuro - Budget hotel with private rooms and 24-hour cafe. Good for groups of two on a budget. Price: ¥7,000 per night. Location: Ikebukuro. Amenities: Private Room, Cafe, Multilingual Staff.", | |
| "metadata": { | |
| "location": "Ikebukuro", | |
| "price": "¥7,000" | |
| } | |
| }, | |
| { | |
| "id": "stay-vn-104", | |
| "text": "Little Hanoi Hostel - Authentic hostel in the heart of the Old Quarter. Offers free breakfast and walking tours. Price: ¥1,200 per night. Location: Hanoi, Vietnam. Amenities: Free Breakfast, Walking Tours, Bicycle Rental.", | |
| "metadata": { | |
| "location": "Hanoi, Vietnam", | |
| "price": "¥1,200" | |
| } | |
| }, | |
| { | |
| "id": "stay-al-105", | |
| "text": "Santi Quanta Hostel - Boutique hostel near the Skanderbeg Square. Known for a social atmosphere and clean facilities. Price: ¥2,000 per night. Location: Tirana, Albania. Amenities: Common Room, City Maps, Air Conditioning.", | |
| "metadata": { | |
| "location": "Tirana, Albania", | |
| "price": "¥2,000" | |
| } | |
| }, | |
| { | |
| "id": "stay-tr-106", | |
| "text": "Cheers Lighthouse - Budget-friendly guesthouse in Sultanahmet with views of the Marmara Sea. Very close to the Blue Mosque. Price: ¥4,500 per night. Location: Istanbul, Turkey. Amenities: Ocean View, Restaurant, Airport Shuttle.", | |
| "metadata": { | |
| "location": "Istanbul, Turkey", | |
| "price": "¥4,500" | |
| } | |
| }, | |
| { | |
| "id": "stay-ro-107", | |
| "text": "Kismet Dao Hostel - Legendary hostel in Brasov. Famous for its backyard BBQ and proximity to the Black Church. Price: ¥2,500 per night. Location: Brasov, Romania. Amenities: Garden BBQ, Lockers, Kitchen Access.", | |
| "metadata": { | |
| "location": "Brasov, Romania", | |
| "price": "¥2,500" |
| "id": "dest-tok-001", | ||
| "text": "Tokyo Budget Overview: Tokyo can be affordable. Use a Pasmo or Suica card for subways. Average daily budget for low-cost travelers is \u00c2\u00a55,000\u00e2\u20ac\u201c\u00c2\u00a58,000. Best free view: Tokyo Metropolitan Government Building in Shinjuku.", | ||
| "metadata": { | ||
| "country": "Japan", | ||
| "topic": "Tokyo Budget Overview" | ||
| } |
There was a problem hiding this comment.
This JSON content includes mojibake sequences (e.g., \u00e2\u20ac\u201c for an en-dash) that will render incorrectly in the demo. Consider replacing them with proper punctuation (e.g., –) and currency symbols to avoid confusing outputs.
on_invoke_tool requires a ToolContext instance — passing None causes silent errors in openai-agents >= 0.13 where ctx.tool_name is accessed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hi @desiorac can you please sign the cla for pr to be reviewed |
Pull Request Checklist
Description
Add an OpenAI Agents SDK cookbook example for Moss semantic search.
Follows the same pattern as the existing CrewAI cookbook — reuses the travel data, provides tool wrappers, an interactive multi-agent demo, and live platform tests.
What's included
moss_openai_agents.py— 6 tool factory functions wrapping MossClient methods as@function_toolcallables:moss_search_tool— semantic search withload_index()before first querymoss_search_with_filter_tool— search with optional metadata filter argumentsmoss_add_docs_tool,moss_delete_docs_tool,moss_get_docs_tool— document CRUDmoss_list_indexes_tool— index listingmoss_tools()— convenience factory for all 6 toolsexample_usage.py— Multi-agent travel planner demo usingagent.as_tool()for orchestrationtest_live.py— Live platform tests covering all tool operationsREADME.md — Setup, quick start, metadata filter usage, local-vs-cloud speed tradeoff
Design decisions
@function_tooldecorators — this keeps the API idiomaticload_index()is called lazily on first search, not at construction timefilter_field+filter_valuestring args (LLM-friendly) rather than a raw dictFixes #92
Type of Change