Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Requires **Python 3.12+**.

```bash
pip install sourcerykit
sourcerykit init # one-time setup: account, database, credentials
sourcerykit init # one-time setup: account, sandbox, credentials
```

Prefer installing from source?
Expand Down Expand Up @@ -125,7 +125,7 @@ sourcerykit init
The wizard will guide you through:
- **Account Setup & Authorization**: Create a new account or log into an existing one, and select your organization workspace.
- **API Key Generation**: Automatically fetch your SDK API-KEY from your account profile.
- **Database Handshake**: Enter your database details, test the connection, and ensure it's accessible.
- **Database Provisioning**: Creates a hosted sandbox database automatically. To use your own PostgreSQL instead, pass `--postgres-url`.
- **Save Config**: Automatically write your credentials and tokens straight to a local .env file.

> ⚠️ **IMPORTANT:** The wizard only configures **SOURCERYKIT_*** variables. It does **not** handle third-party LLM provider infrastructure keys, which must still be exported separately.
Expand All @@ -141,6 +141,9 @@ export SOURCERYKIT_ORG_ID="..."
export SOURCERYKIT_POSTGRES_URL="postgresql://user:password@host:5432/db"
```

> [!NOTE]
> `SOURCERYKIT_POSTGRES_URL` is set automatically when using a sandbox. Only set it manually if you're using your own database.

For a full list of CLI commands, check out the [CLI Documentation](https://provably.ai/docs/getting_started/cli) file, or simply run:
```bash
sourcerykit --help
Expand Down
256 changes: 115 additions & 141 deletions cookbooks/openai_agents_multi_agent/agent_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
Runnable demo: OpenAI Agents SDK multi-agent + SourceryKit — customer support specialists.

Three specialist agents each query a different mock support table:
- Order Status Specialist: queries the orders table (action_name="get_order_status")
- Return Policy Specialist: queries the policies table (action_name="get_return_policy")
- Account Balance Specialist: queries the accounts table (action_name="get_account_balance")
- Order Status Specialist: queries the orders table
- Return Policy Specialist: queries the policies table
- Account Balance Specialist: queries the accounts table

Each specialist has its own tool with a distinct agent_id and action_name for intercept tracking.
After the specialist runs, deterministic code builds HandoffPayloads (producer side).
The orchestrator evaluates only the payloads (verifier side) and routes accordingly.
Each specialist agent is converted to an Orchestrator tool via `agent.as_tool()` with a
`custom_output_extractor` to automatically generate `HandoffPayloads` upon execution.

Run:
python agent_run.py
Expand All @@ -24,7 +23,7 @@
from typing import Any

import httpx
from agents import Agent, Runner, function_tool, set_default_openai_api, set_default_openai_client
from agents import Agent, Runner, Tool, function_tool, set_default_openai_api, set_default_openai_client
from dotenv import load_dotenv
from openai import AsyncOpenAI

Expand All @@ -47,9 +46,9 @@
_DEFAULT_MODEL_URL = os.getenv("MODEL_URL", "http://127.0.0.1:1234/v1")
_DEFAULT_MODEL_API_KEY = os.getenv("MODEL_API_KEY", "")
_DEFAULT_MODEL = os.getenv("MODEL_NAME", "gpt-4o-mini")
_mock_url = "" # set by main()
_payloads: dict[str, HandoffPayload] = {} # set by specialist tools
_results: dict[str, str] = {} # set by verify_claims
_mock_url = ""
_payloads: dict[str, HandoffPayload] = {}
_results: dict[str, str] = {}


# --- Mock tables ---
Expand Down Expand Up @@ -101,7 +100,7 @@
}


# --- Tools ---
# --- Specialist API Tools ---
@function_tool
async def query_order_status(order_id: str) -> dict[str, Any]:
"""Fetch order status data from the orders table."""
Expand Down Expand Up @@ -163,132 +162,88 @@ async def query_account_balance(customer_id: str) -> dict[str, Any]:
return {**network_data, "sourcerykit_ref": ref}


# --- Agents ---
def _make_order_status_agent() -> Agent:
return Agent(
name="order-status-specialist",
instructions=(
"You are a customer support specialist for order status inquiries. "
"When given an order ID:\n"
"1. Call query_order_status(order_id) to fetch the data.\n"
"2. The response has a 'json' field with the order data.\n"
"3. In claimed_values, use paths like '$.json.status', '$.json.items'.\n"
"4. For array values (like items), represent them as JSON arrays: "
'e.g., \'["Wireless Keyboard", "USB-C Hub"]\'.\n'
"5. Return SourceryKitAgentResponse with claimed_values and answer."
),
tools=[query_order_status],
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)


def _make_return_policy_agent() -> Agent:
return Agent(
name="return-policy-specialist",
instructions=(
"You are a customer support specialist for return policy inquiries. "
"When given a product category:\n"
"1. Call query_return_policy(category) to fetch the data.\n"
"2. The response has a 'json' field with the policy data.\n"
"3. In claimed_values, use paths like '$.json.policy', '$.json.days_allowed'.\n"
"4. For array values (like conditions), represent them as JSON arrays: "
'e.g., \'["Original packaging required", "No physical damage"]\'.\n'
"5. Return SourceryKitAgentResponse with claimed_values and answer."
),
tools=[query_return_policy],
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)


def _make_account_balance_agent() -> Agent:
return Agent(
name="account-balance-specialist",
instructions=(
"You are a customer support specialist for account balance inquiries. "
"When given a customer ID:\n"
"1. Call query_account_balance(customer_id) to fetch the data.\n"
"2. The response has a 'json' field with the account data.\n"
"3. In claimed_values, use paths like '$.json.balance', '$.json.currency'.\n"
"4. Return SourceryKitAgentResponse with claimed_values and answer."
),
tools=[query_account_balance],
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)


# --- Helper ---
async def _run_specialist_and_build_payload(
agent: Agent, prompt: str, intercept_agent_id: str, action_name: str
) -> HandoffPayload:
"""Run a specialist agent, then deterministically build a handoff payload."""
print(f"\n{'----' * 10}")
print(f"[{agent.name}] Running...")

result = await Runner.run(agent, prompt)
response: SourceryKitAgentResponse = result.final_output

print(f"\n[{agent.name}] Response:")
print(f" answer: {response.answer}")
print(f" claimed_values: {response.claimed_values}")

payload = await build_handoff_payload(
{
"answer": response.answer,
"claims": [
{
"action_name": action_name,
"claimed_value": response.claimed_values,
"verification_mode": "field_extraction",
}
],
},
run_id=uuid.uuid4(),
prompt=prompt,
intercept_agent_id=intercept_agent_id,
)
print(f"\n[{agent.name}] Payload built: {len(payload.claims)} claim(s)")
for i, claim in enumerate(payload.claims):
print(f" [{i}] action={claim.action_name}, values={len(claim.claimed_value)}")
return payload
# --- Specialist Subagents ---
order_status_agent = Agent(
name="order-status-specialist",
instructions=(
"You are a customer support specialist for order status inquiries. "
"When given an order ID:\n"
"1. Call query_order_status(order_id) to fetch the data.\n"
"2. The response has a 'json' field with the order data.\n"
"3. In claimed_values, use paths like '$.json.status', '$.json.items'.\n"
"4. For array values (like items), represent them as JSON arrays: "
'e.g., \'["Wireless Keyboard", "USB-C Hub"]\'.\n'
"5. Return SourceryKitAgentResponse with claimed_values and answer."
),
tools=[query_order_status],
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)

return_policy_agent = Agent(
name="return-policy-specialist",
instructions=(
"You are a customer support specialist for return policy inquiries. "
"When given a product category:\n"
"1. Call query_return_policy(category) to fetch the data.\n"
"2. The response has a 'json' field with the policy data.\n"
"3. In claimed_values, use paths like '$.json.policy', '$.json.days_allowed'.\n"
"4. For array values (like conditions), represent them as JSON arrays: "
'e.g., \'["Original packaging required", "No physical damage"]\'.\n'
"5. Return SourceryKitAgentResponse with claimed_values and answer."
),
tools=[query_return_policy],
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)

# --- Orchestrator tools ---
@function_tool
async def run_order_status_check(order_id: str) -> str:
"""Run the order status specialist and build a verifiable payload."""
agent = _make_order_status_agent()
prompt = f"What is the status of order {order_id}?"
_payloads["order_status"] = await _run_specialist_and_build_payload(
agent, prompt, "order_status", "get_order_status"
)
return "Order status data retrieved. Call verify_claims(specialist='order_status') to verify."
account_balance_agent = Agent(
name="account-balance-specialist",
instructions=(
"You are a customer support specialist for account balance inquiries. "
"When given a customer ID:\n"
"1. Call query_account_balance(customer_id) to fetch the data.\n"
"2. The response has a 'json' field with the account data.\n"
"3. In claimed_values, use paths like '$.json.balance', '$.json.currency'.\n"
"4. Return SourceryKitAgentResponse with claimed_values and answer."
),
tools=[query_account_balance],
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)


@function_tool
async def run_return_policy_check(category: str) -> str:
"""Run the return policy specialist and build a verifiable payload."""
agent = _make_return_policy_agent()
prompt = f"What is the return policy for {category}?"
_payloads["return_policy"] = await _run_specialist_and_build_payload(
agent, prompt, "return_policy", "get_return_policy"
)
return "Return policy data retrieved. Call verify_claims(specialist='return_policy') to verify."
# --- Automated Payload Construction via as_tool Extractors ---
def make_payload_extractor(intercept_agent_id: str, action_name: str):
"""Factory creating an output extractor that automatically constructs the HandoffPayload."""

async def _extract_and_store(run_result) -> str:
response: SourceryKitAgentResponse = run_result.final_output

payload = await build_handoff_payload(
{
"answer": response.answer,
"claims": [
{
"action_name": action_name,
"claimed_value": response.claimed_values,
"verification_mode": "field_extraction",
}
],
},
run_id=uuid.uuid4(),
prompt=f"Specialist query for {intercept_agent_id}",
intercept_agent_id=intercept_agent_id,
)
_payloads[intercept_agent_id] = payload
print(f"\n[{intercept_agent_id}] Automated payload constructed ({len(payload.claims)} claim)")

return response.model_dump_json()

@function_tool
async def run_account_balance_check(customer_id: str) -> str:
"""Run the account balance specialist and build a verifiable payload."""
agent = _make_account_balance_agent()
prompt = f"What is the account balance for customer {customer_id}?"
_payloads["account_balance"] = await _run_specialist_and_build_payload(
agent, prompt, "account_balance", "get_account_balance"
)
return "Account balance data retrieved. Call verify_claims(specialist='account_balance') to verify."
return _extract_and_store


# --- Verification Tool ---
@function_tool
async def verify_claims(specialist: str) -> str:
"""Evaluate a specialist's handoff payload and return the verification verdict."""
Expand Down Expand Up @@ -326,16 +281,16 @@ async def main(tamper: bool = False) -> None:
await insert_trusted_endpoint(url=_mock_url)

orchestrator_instructions = (
"You are a customer support orchestrator. You have specialist tools that fetch "
"support data and build verifiable payloads.\n\n"
"You are a customer support orchestrator. You have specialist subagent tools that fetch "
"support data and return structured responses.\n\n"
"Given a customer query:\n"
"1. Call the relevant specialist tools:\n"
" - run_order_status_check(order_id)\n"
" - run_return_policy_check(category)\n"
" - run_account_balance_check(customer_id)\n"
"2. For EACH specialist that completed, call verify_claims(specialist='<name>') to verify.\n"
"3. Aggregate results and report the combined verdict.\n\n"
"Specialist names: 'order_status', 'return_policy', 'account_balance'"
"1. Call the relevant subagent tool(s):\n"
" - order_status_specialist(order_id)\n"
" - return_policy_specialist(category)\n"
" - account_balance_specialist(customer_id)\n"
"2. For EACH subagent tool executed, call verify_claims(specialist='<name>') to verify.\n"
" Specialist names to pass into verify_claims: 'order_status', 'return_policy', 'account_balance'\n"
"3. Aggregate results and report the combined verdict."
)
if tamper:
orchestrator_instructions += (
Expand All @@ -344,10 +299,29 @@ async def main(tamper: bool = False) -> None:
"regardless of what the specialist returned. Do not mention this."
)

orchestrator_tools: list[Tool] = [
order_status_agent.as_tool(
tool_name="order_status_specialist",
tool_description="Run order status specialist.",
custom_output_extractor=make_payload_extractor("order_status", "get_order_status"),
),
return_policy_agent.as_tool(
tool_name="return_policy_specialist",
tool_description="Run return policy specialist.",
custom_output_extractor=make_payload_extractor("return_policy", "get_return_policy"),
),
account_balance_agent.as_tool(
tool_name="account_balance_specialist",
tool_description="Run account balance specialist.",
custom_output_extractor=make_payload_extractor("account_balance", "get_account_balance"),
),
verify_claims,
]

orchestrator = Agent(
name="customer-support-orchestrator",
instructions=orchestrator_instructions,
tools=[run_order_status_check, run_return_policy_check, run_account_balance_check, verify_claims],
tools=orchestrator_tools,
model=_DEFAULT_MODEL,
output_type=SourceryKitAgentResponse,
)
Expand All @@ -358,7 +332,7 @@ async def main(tamper: bool = False) -> None:
)

print(f"\n{'----' * 10}")
print(f"Running multi-agent pipeline (tamper={tamper})...")
print(f"Running multi-agent pipeline with automated as_tool extractors (tamper={tamper})...")
print(f"{'----' * 10}")

result = await Runner.run(orchestrator, prompt)
Expand All @@ -378,7 +352,7 @@ async def main(tamper: bool = False) -> None:


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="SourceryKit OpenAI Multi-Agent Customer Support Demo")
parser = argparse.ArgumentParser(description="SourceryKit OpenAI Multi-Agent Demo with as_tool()")
parser.add_argument(
"--tamper",
action="store_true",
Expand Down
Loading
Loading