Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ee69932
feat(alk): platform-generated scenarios + canonical voice prompts + L…
azain-commits Jul 28, 2026
67033fa
feat(alk): wire scenario generation into CLI + Studio downloader upgr…
azain-commits Jul 28, 2026
c063404
feat(sim): stage-0/5 protocol layer, evidence sources, matrix runner,…
azain-commits Jul 29, 2026
e16933a
fix(sim): repair LiveKit SIP transport, wire Vapi originator, tighten…
azain-commits Jul 29, 2026
3cf8c8f
feat(simulate): add direct voice SDK workflow
azain-commits Jul 30, 2026
963a399
feat(simulate): separate voice target from FutureAGI LiveKit runtime …
azain-commits Jul 30, 2026
35d6525
fix(studio): omit scenario-only agent flag
azain-commits Jul 30, 2026
d2b1420
feat(simulate): complete provider acceptance flows
azain-commits Jul 31, 2026
b53e4a7
feat(simulate): pool-aware LiveKit engine + provider acceptance harde…
azain-commits Aug 4, 2026
1999a1e
fix issues on livekit engine, add support for posting simulate data t…
azain-commits Aug 5, 2026
cf3f3eb
feat(simulate): real platform submission in FutureAGIResultSink
azain-commits Aug 5, 2026
6484b2b
feat(simulate): submit target-agent token usage + cost to platform
azain-commits Aug 5, 2026
c239ace
fix(evals): exclude non-applicable metrics from agent-report aggregate
azain-commits Aug 6, 2026
e0d9e2e
feat(evals): live litellm/Vertex provider for eval + optimize-eval su…
azain-commits Aug 6, 2026
ed86c24
fix(opt): configurable task_model, drop hardcoded gpt-4o-mini/gpt-5-mini
azain-commits Aug 6, 2026
01f6904
feat(evals,opt): wire platform evals + all optimizers into agent-learn
azain-commits Aug 6, 2026
bcd1a76
fix(simulate): tool-call fidelity + per-turn latency in chat runs
azain-commits Aug 10, 2026
c695347
refactor(simulate): gym-model runtime — adapter registries + world_ki…
azain-commits Aug 10, 2026
d1208ce
feat(simulate): SDK hosted-runner package (child entrypoint + job + t…
azain-commits Aug 10, 2026
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
31 changes: 31 additions & 0 deletions .github/workflows/sdk-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: SDK smoke

on:
push:
pull_request:

jobs:
clean-install:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install package
run: python -m pip install .
- name: Run doctor
run: agent-learn doctor --quiet
- name: Run local text simulation
env:
AGENT_LEARNING_RUN_EXAMPLE_KEY: smoke
run: >-
agent-learn simulation run examples/run_manifest.json
--output smoke-report.json --quiet
- name: Verify simulation report
run: >-
python -c "import json, pathlib;
report=json.loads(pathlib.Path('smoke-report.json').read_text());
assert report['status'] == 'ran' and report['report']['results']"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ coverage/
.DS_Store
.env
.env.*
!**/.env.example
artifacts/
!src/fi/simulate/artifacts/
!src/fi/simulate/artifacts/*.py
examples/artifacts/
706 changes: 706 additions & 0 deletions examples/agent_learning_sdk_demo_v2.ipynb

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions examples/build_delivery_support_suite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any

from fi.alk import studio
from fi.simulate.simulation.models import Scenario

_CASES = (
{
"name": "Morgan",
"role": "busy customer",
"order_id": "DS-1001",
"situation": "My delivery for order DS-1001 has not arrived and I need its status.",
"temperament": {"rajas": 0.4, "sattva": 0.9, "tamas": 0.1},
"style_notes": ["polite", "concise"],
},
{
"name": "Avery",
"role": "impatient customer",
"order_id": "DS-1002",
"situation": "Order DS-1002 is a day late and I need a concrete arrival time.",
"temperament": {"rajas": 0.9, "sattva": 0.4, "tamas": 0.1},
"style_notes": ["urgent", "direct"],
},
{
"name": "Jordan",
"role": "privacy-conscious customer",
"order_id": "DS-1003",
"situation": "I want the status of order DS-1003 but I do not want to share unrelated personal data.",
"temperament": {"rajas": 0.3, "sattva": 0.7, "tamas": 0.4},
"style_notes": ["cautious", "measured"],
},
{
"name": "Riley",
"role": "detail-oriented customer",
"order_id": "DS-1004",
"situation": "Please explain the current location and next delivery step for order DS-1004.",
"temperament": {"rajas": 0.5, "sattva": 0.8, "tamas": 0.2},
"style_notes": ["precise", "asks follow-up questions"],
},
{
"name": "Casey",
"role": "frustrated customer",
"order_id": "DS-1005",
"situation": "Order DS-1005 missed its promised window twice and I need this resolved.",
"temperament": {"rajas": 0.8, "sattva": 0.3, "tamas": 0.3},
"style_notes": ["frustrated", "expects accountability"],
},
{
"name": "Taylor",
"role": "cooperative customer",
"order_id": "DS-1006",
"situation": "I am checking whether order DS-1006 will arrive before I leave town.",
"temperament": {"rajas": 0.4, "sattva": 0.95, "tamas": 0.1},
"style_notes": ["cooperative", "clear"],
},
{
"name": "Quinn",
"role": "skeptical customer",
"order_id": "DS-1007",
"situation": "The tracking page for order DS-1007 has not changed and I need evidence of its status.",
"temperament": {"rajas": 0.6, "sattva": 0.5, "tamas": 0.3},
"style_notes": ["skeptical", "requests confirmation"],
},
{
"name": "Parker",
"role": "distracted customer",
"order_id": "DS-1008",
"situation": "I only have a minute to check the delivery status of order DS-1008.",
"temperament": {"rajas": 0.7, "sattva": 0.6, "tamas": 0.2},
"style_notes": ["brief", "easily distracted"],
},
{
"name": "Cameron",
"role": "patient customer",
"order_id": "DS-1009",
"situation": "Order DS-1009 is delayed and I would like to understand the revised schedule.",
"temperament": {"rajas": 0.2, "sattva": 0.9, "tamas": 0.3},
"style_notes": ["patient", "thoughtful"],
},
{
"name": "Drew",
"role": "escalation-prone customer",
"order_id": "DS-1010",
"situation": "I need an immediate status and escalation path for missing order DS-1010.",
"temperament": {"rajas": 0.95, "sattva": 0.25, "tamas": 0.2},
"style_notes": ["forceful", "escalates when answers are vague"],
},
)


def build_suite() -> Scenario:
outcome = "The delivery status, expected arrival, and next step are confirmed."
personas = [
studio.build_persona(
name=case["name"],
role=case["role"],
situation=case["situation"],
outcome=outcome,
style_notes=case["style_notes"],
temperament=case["temperament"],
knowledge=[
{
"key": "order_number",
"value": f"My order number is {case['order_id']}",
"disclosure": "volunteer",
}
],
evidence_class="schema_sampled",
)
for case in _CASES
]
for persona in personas:
validation = studio.validate_persona(persona)
if validation["status"] != "valid":
raise ValueError(f"persona validation failed: {persona.identity.name}")
bias = studio.bias_lint(personas)
if bias["status"] != "passed":
raise ValueError("delivery support suite failed bias lint")
return Scenario(
name="delivery-support-studio-suite",
description="Ten typed delivery-status callers for text and WebRTC acceptance.",
kind="task",
dataset=personas,
coverage={
"intents": ["delivery_status", "arrival_estimate", "next_step"],
"personas": [persona.version for persona in personas],
"perturbations": ["urgency", "privacy", "skepticism", "escalation"],
"tool_obligations": ["allow:lookup_delivery"],
},
constraints={
"declared_tools": ["lookup_delivery"],
"max_user_knowledge": ["order_number"],
},
)


def write_suite(path: str | Path) -> dict[str, Any]:
scenario = build_suite()
payload = scenario.model_dump(mode="json", exclude_none=True)
destination = Path(path).expanduser().resolve()
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
return payload


if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit("usage: python examples/build_delivery_support_suite.py OUTPUT.json")
write_suite(sys.argv[1])
155 changes: 155 additions & 0 deletions examples/sdk_actor_source_tool_calling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Drop in a tool-calling agent via the ActorSource + Environment abstractions.

Offline, deterministic, no credentials. Showcases the refactored gym model end
to end:

* an ``EnvironmentAdapter`` world (``RefundWorld``) that declares an action space
(``lookup_order`` / ``approve_refund``) and owns state,
* a plain tool-calling agent class dropped in through the ``factory``
**ActorSource** (``target``/``factory`` — the same vocabulary a manifest
``agent:`` block uses), resolved via the one endpoint registry,
* driven through ``SimulationRunner`` — the same spine chat and voice use.

The agent calls ``approve_refund``; the world executes it and moves to
``status: approved``; we assert the tool actually drove the world.

Run: python examples/sdk_actor_source_tool_calling.py artifacts/actor-tool.json
"""

from __future__ import annotations

import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Any, Mapping, Optional

from fi.simulate.agent.wrapper import AgentInput, AgentResponse
from fi.simulate.endpoints.profiles import get_profile
from fi.simulate.environment import EnvironmentAdapter, EnvironmentSnapshot, ToolExecutionResult
from fi.simulate.runtime import (
AgentEndpointSpec,
EnvironmentSpec,
RunStatus,
SimulationSpec,
SimulatorPolicySpec,
)
from fi.simulate.runtime.runner import SimulationRunner
from fi.simulate.simulation.models import Persona, Scenario

_TOOL_SCHEMAS = [
{"name": "lookup_order", "description": "Look up an order by id."},
{"name": "approve_refund", "description": "Approve a refund for an order."},
]


class RefundWorld(EnvironmentAdapter):
"""A tiny executable world: two tools + refund state."""

name = "refund_world"

def __init__(self) -> None:
self.state: dict[str, Any] = {"refund": {"status": "pending"}}

def reset(self, **_context: Any) -> EnvironmentSnapshot:
self.state = {"refund": {"status": "pending"}}
return EnvironmentSnapshot(tools=list(_TOOL_SCHEMAS), state=dict(self.state))

def handle_tool_call(
self, tool_call: Mapping[str, Any], **_context: Any
) -> Optional[ToolExecutionResult]:
name = tool_call.get("name") or (tool_call.get("function") or {}).get("name")
call_id = tool_call.get("id") or tool_call.get("tool_call_id")
if name == "lookup_order":
return ToolExecutionResult(
tool_call_id=call_id, tool_name=name,
content="order A1: eligible for refund", result={"eligible": True},
)
if name == "approve_refund":
self.state["refund"]["status"] = "approved"
return ToolExecutionResult(
tool_call_id=call_id, tool_name=name,
content="refund approved", result={"status": "approved"},
state_updates={"refund": {"status": "approved"}},
)
return None


class ToolCallingRefundAgent:
"""Scripted target agent: looks up the order, then approves the refund.

Dropped in via the ``factory`` ActorSource — the harness sets up the
environment around it; the agent just acts in the action space.
"""

async def call(self, agent_input: AgentInput) -> AgentResponse:
turn = agent_input.turn_index
if turn == 0:
return AgentResponse(
content="Let me look up your order.",
tool_calls=[{"id": "c0", "name": "lookup_order",
"arguments": {"order_id": "A1"}}],
)
if turn == 1:
return AgentResponse(
content="It's eligible — approving the refund now.",
tool_calls=[{"id": "c1", "name": "approve_refund",
"arguments": {"order_id": "A1"}}],
)
return AgentResponse(content="Your refund is approved. Anything else?")


def run(output_path: str | os.PathLike[str]) -> dict[str, Any]:
# Make this module importable by "module:attr" so the ActorSource factory can
# resolve the agent the same way a real job would.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
module_stem = Path(__file__).stem

# Drop the agent in through the factory ActorSource (local resolution).
target = get_profile("factory").resolve_target(
{"target": f"{module_stem}:ToolCallingRefundAgent", "factory": True},
hosted=False,
)

spec = SimulationSpec(
run_id="run_actor_tool_calling",
environment=EnvironmentSpec(
adapter="chat", world_kind="conversation",
config={"max_turns": 3, "min_turns": 1},
),
target=AgentEndpointSpec(adapter="factory"),
simulator=SimulatorPolicySpec(adapter="synthetic_user"),
scenario=Scenario(
name="refund",
dataset=[Persona(
persona={"name": "Sam"},
situation="My order A1 arrived damaged.",
outcome="the refund is approved",
)],
),
)

world = RefundWorld()
report = asyncio.run(SimulationRunner().run(spec, target=target, environment=world))

tool_ran = world.state["refund"]["status"] == "approved"
data = {
"kind": "agent-learning.actor-source-example.v1",
"status": "passed" if (report.status == RunStatus.COMPLETED and tool_ran) else "failed",
"run_status": report.status.value,
"world_final_state": world.state,
"tool_drove_world": tool_ran,
"transcript": report.test_cases[0].result.transcript if report.test_cases else "",
}
out = Path(output_path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(data, indent=2), encoding="utf-8")
return data


if __name__ == "__main__":
target_path = sys.argv[1] if len(sys.argv) > 1 else "artifacts/actor-tool.json"
result = run(target_path)
print(json.dumps(result, indent=2))
sys.exit(0 if result["status"] == "passed" else 1)
Loading
Loading