-
Notifications
You must be signed in to change notification settings - Fork 43
feat(simulate): platform scenarios + LiveKit SIP transport + provider acceptance flows #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
azain-commits
wants to merge
37
commits into
release/v1-agent-learning-kit-dev
Choose a base branch
from
feat/platform-scenarios-livekit-sip
base: release/v1-agent-learning-kit-dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
ee69932
feat(alk): platform-generated scenarios + canonical voice prompts + L…
azain-commits 67033fa
feat(alk): wire scenario generation into CLI + Studio downloader upgr…
azain-commits c063404
feat(sim): stage-0/5 protocol layer, evidence sources, matrix runner,…
azain-commits e16933a
fix(sim): repair LiveKit SIP transport, wire Vapi originator, tighten…
azain-commits 3cf8c8f
feat(simulate): add direct voice SDK workflow
azain-commits 963a399
feat(simulate): separate voice target from FutureAGI LiveKit runtime …
azain-commits 35d6525
fix(studio): omit scenario-only agent flag
azain-commits d2b1420
feat(simulate): complete provider acceptance flows
azain-commits b53e4a7
feat(simulate): pool-aware LiveKit engine + provider acceptance harde…
azain-commits 1999a1e
fix issues on livekit engine, add support for posting simulate data t…
azain-commits cf3f3eb
feat(simulate): real platform submission in FutureAGIResultSink
azain-commits 6484b2b
feat(simulate): submit target-agent token usage + cost to platform
azain-commits c239ace
fix(evals): exclude non-applicable metrics from agent-report aggregate
azain-commits e0d9e2e
feat(evals): live litellm/Vertex provider for eval + optimize-eval su…
azain-commits ed86c24
fix(opt): configurable task_model, drop hardcoded gpt-4o-mini/gpt-5-mini
azain-commits 01f6904
feat(evals,opt): wire platform evals + all optimizers into agent-learn
azain-commits bcd1a76
fix(simulate): tool-call fidelity + per-turn latency in chat runs
azain-commits c695347
refactor(simulate): gym-model runtime — adapter registries + world_ki…
azain-commits d1208ce
feat(simulate): SDK hosted-runner package (child entrypoint + job + t…
azain-commits e573c93
feat(simulate): per-persona TTS voice selection
azain-commits 3db32eb
fix(simulate): send internal Bearer secret alongside FI keys on submit
azain-commits ee26069
fix(simulate): enable preemptive generation to cut voice turn latency
azain-commits 60a924c
feat(voice): run a scenario's cases concurrently, bounded by agent ca…
azain-commits 3f1413d
feat(simulate): stream hosted voice cases to the platform as each fin…
azain-commits c95629f
fix(voice): run sims to natural end + stop transcript role reversal
azain-commits d13f8c2
fix(livekit): send empty dispatch metadata to external target agents …
azain-commits b93f2c9
fix(livekit): capture target-agent turns so webrtc sims run a real co…
azain-commits 1fc2a40
fix(simulate): drive agent_first conversations from the target greeting
azain-commits 0f4b5de
Revert "fix(simulate): drive agent_first conversations from the targe…
azain-commits fa87079
fix(simulate): capture the target's opening greeting in agent_first
azain-commits 7f080e6
feat(simulate): stereo voice recording (customer left / assistant right)
azain-commits 5de580c
feat(simulate): streaming Gemini TTS for the simulator voice (multili…
azain-commits bfc6caf
fix(simulate): capture final target turn, truncate interrupted turns,…
azain-commits 34711b7
fix(simulate): commit target turns directly so the trailing closing i…
azain-commits 0d75d91
fix(simulate): capture target closing via independent stream, not the…
azain-commits 82f23a8
fix(simulate): also wait for the simulator's own final turn before sn…
azain-commits f429e1b
fix(simulate): delete the room on conversation end so the call actual…
azain-commits File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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']" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow doesn't run the tests this PR adds.
Credit first: this file is new in this PR, so it takes the repo from no CI to some CI. That's the right direction.
But as written it installs the package, runs
agent-learn doctor, and executes one local text simulation. Nopytest, no lint. So roughly 3,000 lines of new tests —test_livekit_engine.py(1,915),test_manifest_engine_dispatch.py(521),test_acceptance_regressions.py(490) — never execute in CI.That's the mechanism behind the five failures above surviving six commits: nothing was watching.
A step like:
would close it. Two caveats from running the suite locally:
tests/test_config_and_facades.pyhangs (>3 min, also on the base branch — pre-existing, worth an--ignoreor a timeout), andtests/test_phase7_persona_studio.pyhas 2 network-dependent failures that also reproduce on base. Neither is introduced here, but both need handling before apytestgate goes green.