agent-learning-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_learning_kit-0.1.0.dist-info/METADATA +381 -0
- agent_learning_kit-0.1.0.dist-info/RECORD +642 -0
- agent_learning_kit-0.1.0.dist-info/WHEEL +4 -0
- agent_learning_kit-0.1.0.dist-info/entry_points.txt +5 -0
- agent_learning_kit-0.1.0.dist-info/licenses/LICENSE +173 -0
- agent_learning_kit-0.1.0.dist-info/licenses/NOTICE +7 -0
- fi/__init__.py +5 -0
- fi/alk/__init__.py +57 -0
- fi/alk/_facade.py +31 -0
- fi/alk/_module_alias.py +68 -0
- fi/alk/_paths.py +14 -0
- fi/alk/_schema.py +522 -0
- fi/alk/actions.py +727 -0
- fi/alk/bench/__init__.py +517 -0
- fi/alk/bench/_codeexec.py +213 -0
- fi/alk/bench/_coding.py +215 -0
- fi/alk/bench/_docker.py +237 -0
- fi/alk/bench/_grader.py +286 -0
- fi/alk/bench/_pull.py +212 -0
- fi/alk/bench/_voice.py +147 -0
- fi/alk/capabilities.py +627 -0
- fi/alk/cli.py +6396 -0
- fi/alk/config.py +130 -0
- fi/alk/cua_loop.py +562 -0
- fi/alk/evals.py +2351 -0
- fi/alk/extensions.py +163 -0
- fi/alk/harness/ARCHITECTURE.md +231 -0
- fi/alk/harness/DESIGN.md +246 -0
- fi/alk/harness/ENVIRONMENT_CONFORMANCE.md +127 -0
- fi/alk/harness/HOW-IT-WORKS.md +297 -0
- fi/alk/harness/IMPLEMENTATION_AND_VALIDATION_STATUS.md +229 -0
- fi/alk/harness/README.md +417 -0
- fi/alk/harness/__init__.py +77 -0
- fi/alk/harness/__main__.py +3 -0
- fi/alk/harness/amend.py +312 -0
- fi/alk/harness/artifacts.py +319 -0
- fi/alk/harness/authoring_entrypoint.py +189 -0
- fi/alk/harness/authoring_runtime_validation.py +267 -0
- fi/alk/harness/backends/README.md +43 -0
- fi/alk/harness/backends/__init__.py +122 -0
- fi/alk/harness/backends/base.py +241 -0
- fi/alk/harness/backends/claude.py +211 -0
- fi/alk/harness/backends/files.py +182 -0
- fi/alk/harness/backends/vertex_gemini.py +457 -0
- fi/alk/harness/background_noise.py +95 -0
- fi/alk/harness/build.py +385 -0
- fi/alk/harness/bundle.py +593 -0
- fi/alk/harness/bundle_author_v2.py +1831 -0
- fi/alk/harness/bundle_v2.py +719 -0
- fi/alk/harness/call_runner.py +1440 -0
- fi/alk/harness/callback_http_adapter.py +111 -0
- fi/alk/harness/catalogue.py +287 -0
- fi/alk/harness/chat.py +428 -0
- fi/alk/harness/chat_call_runner.py +506 -0
- fi/alk/harness/checks.py +136 -0
- fi/alk/harness/cli.py +1354 -0
- fi/alk/harness/config.py +338 -0
- fi/alk/harness/contract.py +718 -0
- fi/alk/harness/credentials.py +674 -0
- fi/alk/harness/data/persona_vocabulary.json +111 -0
- fi/alk/harness/environment.py +99 -0
- fi/alk/harness/environment_plan.py +168 -0
- fi/alk/harness/events.py +125 -0
- fi/alk/harness/executor.py +304 -0
- fi/alk/harness/folder.py +234 -0
- fi/alk/harness/generated_runtime.py +815 -0
- fi/alk/harness/github.py +72 -0
- fi/alk/harness/hosted_authoring_entrypoint.py +183 -0
- fi/alk/harness/hosted_entrypoint.py +2402 -0
- fi/alk/harness/hosted_scheduler.py +2218 -0
- fi/alk/harness/job.py +426 -0
- fi/alk/harness/judge.py +184 -0
- fi/alk/harness/livekit_source.py +50 -0
- fi/alk/harness/livekit_tool_trace_bootstrap.py +71 -0
- fi/alk/harness/observability.py +208 -0
- fi/alk/harness/outbound.py +3252 -0
- fi/alk/harness/packaging.py +515 -0
- fi/alk/harness/persona_guides.py +157 -0
- fi/alk/harness/platform.py +692 -0
- fi/alk/harness/process_preflight.py +764 -0
- fi/alk/harness/process_runtime.py +5670 -0
- fi/alk/harness/prove.py +425 -0
- fi/alk/harness/provider_import.py +703 -0
- fi/alk/harness/provider_lifecycle.py +392 -0
- fi/alk/harness/provision.py +2896 -0
- fi/alk/harness/reception.py +147 -0
- fi/alk/harness/retell_chat_call_runner.py +373 -0
- fi/alk/harness/run/__init__.py +296 -0
- fi/alk/harness/run/alk.py +184 -0
- fi/alk/harness/run/call.py +162 -0
- fi/alk/harness/run/conversation.py +264 -0
- fi/alk/harness/run/data/voices_by_language_and_gender.json +693 -0
- fi/alk/harness/run/evidence.py +195 -0
- fi/alk/harness/run/grade.py +598 -0
- fi/alk/harness/run/live.py +297 -0
- fi/alk/harness/run/models.py +56 -0
- fi/alk/harness/run/platform_evals.py +227 -0
- fi/alk/harness/run/sdk_voice.py +130 -0
- fi/alk/harness/run/simulation.py +1209 -0
- fi/alk/harness/run/stage.py +91 -0
- fi/alk/harness/run/targets.py +508 -0
- fi/alk/harness/run/tools.py +601 -0
- fi/alk/harness/run/voice.py +340 -0
- fi/alk/harness/runtime.py +172 -0
- fi/alk/harness/sandbox_server.py +2011 -0
- fi/alk/harness/sandbox_worker.py +44 -0
- fi/alk/harness/scenario.py +1048 -0
- fi/alk/harness/scenario_source.py +879 -0
- fi/alk/harness/scenario_tools.py +1143 -0
- fi/alk/harness/scenarios.py +915 -0
- fi/alk/harness/secrets.py +168 -0
- fi/alk/harness/service_catalog.py +97 -0
- fi/alk/harness/session.py +391 -0
- fi/alk/harness/sessions.py +372 -0
- fi/alk/harness/simulator.py +76 -0
- fi/alk/harness/simulator_voice.py +928 -0
- fi/alk/harness/skills/build-environment/SKILL.md +538 -0
- fi/alk/harness/skills/harness.md +131 -0
- fi/alk/harness/skills/kinds/chat.md +48 -0
- fi/alk/harness/skills/kinds/voice-voicemail.md +63 -0
- fi/alk/harness/skills/kinds/voice.md +59 -0
- fi/alk/harness/skills/plan-suite/SKILL.md +103 -0
- fi/alk/harness/skills/provision-environment/SKILL.md +136 -0
- fi/alk/harness/skills/run-scenarios/SKILL.md +112 -0
- fi/alk/harness/skills/understand-agent/SKILL.md +251 -0
- fi/alk/harness/skills/write-scenarios/SKILL.md +606 -0
- fi/alk/harness/skills/write-scenarios/references/refusals.md +28 -0
- fi/alk/harness/skills/write-scenarios/references/world-api.md +92 -0
- fi/alk/harness/source_data_invariants.py +444 -0
- fi/alk/harness/source_tool_evidence.py +79 -0
- fi/alk/harness/sources.py +253 -0
- fi/alk/harness/spend.py +140 -0
- fi/alk/harness/tool_trace_proxy.py +104 -0
- fi/alk/harness/tools.py +1018 -0
- fi/alk/harness/understand.py +169 -0
- fi/alk/harness/voicemail_audio.py +74 -0
- fi/alk/harness/world/__init__.py +33 -0
- fi/alk/harness/world/errors.py +68 -0
- fi/alk/harness/world/expectations.py +91 -0
- fi/alk/harness/world/handle.py +538 -0
- fi/alk/harness/world/kinds.py +196 -0
- fi/alk/harness/world/mutate.py +186 -0
- fi/alk/harness/world/probe.py +413 -0
- fi/alk/harness/world/provision.py +511 -0
- fi/alk/harness/world/provisioned.py +191 -0
- fi/alk/harness/world/runtime.py +616 -0
- fi/alk/harness/world/snapshot.py +288 -0
- fi/alk/harness/world/stores/__init__.py +305 -0
- fi/alk/harness/world/stores/container.py +215 -0
- fi/alk/harness/world/stores/inprocess.py +346 -0
- fi/alk/harness/world/stores/postgres.py +481 -0
- fi/alk/harness/world/stores/prove.py +202 -0
- fi/alk/harness/world/stores/sqlite.py +245 -0
- fi/alk/harness/world/stores/written.py +182 -0
- fi/alk/harness/world/tools.py +1516 -0
- fi/alk/harness/world/workspace.py +144 -0
- fi/alk/image_loop.py +453 -0
- fi/alk/image_perturb.py +241 -0
- fi/alk/improve.py +274 -0
- fi/alk/live/__init__.py +154 -0
- fi/alk/live/_attribution.py +184 -0
- fi/alk/live/_capture.py +264 -0
- fi/alk/live/_codec.py +391 -0
- fi/alk/live/_contract.py +134 -0
- fi/alk/live/_loopback.py +316 -0
- fi/alk/live/_perturb.py +449 -0
- fi/alk/live/_runner.py +386 -0
- fi/alk/live/_stats.py +561 -0
- fi/alk/live/_transcript.py +240 -0
- fi/alk/live/_workers/__init__.py +9 -0
- fi/alk/live/_workers/a2a_worker.py +316 -0
- fi/alk/live/_workers/langgraph_worker.py +217 -0
- fi/alk/live/_workers/livekit_worker.py +207 -0
- fi/alk/live/_workers/mcp_loopback_server.py +46 -0
- fi/alk/live/_workers/mcp_worker.py +158 -0
- fi/alk/live/_workers/pipecat_worker.py +189 -0
- fi/alk/live/a2a_lane.py +138 -0
- fi/alk/live/langgraph_lane.py +339 -0
- fi/alk/live/livekit_lane.py +376 -0
- fi/alk/live/mcp_lane.py +172 -0
- fi/alk/live/pipecat_lane.py +341 -0
- fi/alk/live/voice_redteam.py +494 -0
- fi/alk/loss.py +306 -0
- fi/alk/optimize.py +36260 -0
- fi/alk/practice/__init__.py +51 -0
- fi/alk/practice/_assess.py +103 -0
- fi/alk/practice/_budget.py +81 -0
- fi/alk/practice/_calibrate.py +69 -0
- fi/alk/practice/_capstone.py +86 -0
- fi/alk/practice/_contract.py +91 -0
- fi/alk/practice/_diagnose.py +79 -0
- fi/alk/practice/_drill.py +196 -0
- fi/alk/practice/_experiment.py +720 -0
- fi/alk/practice/_schedule.py +102 -0
- fi/alk/practice/_store.py +194 -0
- fi/alk/practice/_trainer.py +245 -0
- fi/alk/practice/_update.py +125 -0
- fi/alk/redteam.py +2621 -0
- fi/alk/rewardhack.py +237 -0
- fi/alk/simulate.py +10351 -0
- fi/alk/studio/__init__.py +82 -0
- fi/alk/studio/_bias.py +314 -0
- fi/alk/studio/_calibration.py +522 -0
- fi/alk/studio/_coverage.py +262 -0
- fi/alk/studio/_download.py +665 -0
- fi/alk/studio/_fidelity_attack.py +114 -0
- fi/alk/studio/_generate.py +652 -0
- fi/alk/studio/_library.py +370 -0
- fi/alk/studio/_scan.py +134 -0
- fi/alk/studio/_upgrade.py +42 -0
- fi/alk/studio/_vendor.py +172 -0
- fi/alk/suite.py +4200 -0
- fi/alk/tasks.py +828 -0
- fi/alk/telemetry/__init__.py +149 -0
- fi/alk/telemetry/_contract.py +141 -0
- fi/alk/telemetry/_emit.py +182 -0
- fi/alk/telemetry/_ledger.py +296 -0
- fi/alk/telemetry/_queue.py +127 -0
- fi/alk/telemetry/_row.py +294 -0
- fi/alk/telemetry/_run.py +233 -0
- fi/alk/telemetry/_sync.py +193 -0
- fi/alk/telemetry/_url.py +119 -0
- fi/alk/trinity.py +49397 -0
- fi/alk/voice_loop.py +174 -0
- fi/api/__init__.py +1 -0
- fi/api/auth.py +137 -0
- fi/api/types.py +29 -0
- fi/cli/__init__.py +9 -0
- fi/cli/assertions/__init__.py +25 -0
- fi/cli/assertions/conditions.py +76 -0
- fi/cli/assertions/evaluator.py +286 -0
- fi/cli/assertions/exit_codes.py +20 -0
- fi/cli/assertions/parser.py +131 -0
- fi/cli/assertions/reporter.py +194 -0
- fi/cli/commands/__init__.py +9 -0
- fi/cli/commands/config.py +165 -0
- fi/cli/commands/export.py +208 -0
- fi/cli/commands/init.py +112 -0
- fi/cli/commands/list_cmd.py +213 -0
- fi/cli/commands/run.py +486 -0
- fi/cli/commands/validate.py +173 -0
- fi/cli/commands/view.py +424 -0
- fi/cli/config/__init__.py +6 -0
- fi/cli/config/defaults.py +206 -0
- fi/cli/config/loader.py +155 -0
- fi/cli/config/schema.py +174 -0
- fi/cli/main.py +78 -0
- fi/cli/output/__init__.py +6 -0
- fi/cli/output/formatters.py +106 -0
- fi/cli/output/reporters.py +46 -0
- fi/cli/storage/__init__.py +5 -0
- fi/cli/storage/run_history.py +249 -0
- fi/cli/utils/__init__.py +5 -0
- fi/cli/utils/console.py +44 -0
- fi/evals/__init__.py +131 -0
- fi/evals/autoeval/__init__.py +137 -0
- fi/evals/autoeval/analyzer.py +211 -0
- fi/evals/autoeval/config.py +244 -0
- fi/evals/autoeval/export.py +213 -0
- fi/evals/autoeval/interactive.py +283 -0
- fi/evals/autoeval/pipeline.py +625 -0
- fi/evals/autoeval/prompts.py +139 -0
- fi/evals/autoeval/recommender.py +242 -0
- fi/evals/autoeval/rules.py +589 -0
- fi/evals/autoeval/templates.py +299 -0
- fi/evals/autoeval/types.py +232 -0
- fi/evals/core/__init__.py +16 -0
- fi/evals/core/cloud_registry.py +184 -0
- fi/evals/core/engines.py +368 -0
- fi/evals/core/evaluate.py +319 -0
- fi/evals/core/judge_prompt.py +90 -0
- fi/evals/core/prompt_generator.py +83 -0
- fi/evals/core/registry.py +57 -0
- fi/evals/core/result.py +55 -0
- fi/evals/evaluator.py +721 -0
- fi/evals/execution.py +168 -0
- fi/evals/feedback/__init__.py +32 -0
- fi/evals/feedback/calibrator.py +160 -0
- fi/evals/feedback/collector.py +214 -0
- fi/evals/feedback/hooks.py +81 -0
- fi/evals/feedback/retriever.py +128 -0
- fi/evals/feedback/store.py +272 -0
- fi/evals/feedback/types.py +99 -0
- fi/evals/framework/README.md +79 -0
- fi/evals/framework/__init__.py +267 -0
- fi/evals/framework/backends/Dockerfile.eval-runner +33 -0
- fi/evals/framework/backends/__init__.py +99 -0
- fi/evals/framework/backends/_container.py +141 -0
- fi/evals/framework/backends/_utils.py +145 -0
- fi/evals/framework/backends/base.py +223 -0
- fi/evals/framework/backends/celery_backend.py +417 -0
- fi/evals/framework/backends/celery_worker.py +78 -0
- fi/evals/framework/backends/kubernetes_backend.py +665 -0
- fi/evals/framework/backends/ray_backend.py +521 -0
- fi/evals/framework/backends/temporal.py +350 -0
- fi/evals/framework/backends/temporal_worker.py +126 -0
- fi/evals/framework/backends/thread_pool.py +286 -0
- fi/evals/framework/context.py +258 -0
- fi/evals/framework/enrichment.py +306 -0
- fi/evals/framework/evals/__init__.py +68 -0
- fi/evals/framework/evals/agentic.py +399 -0
- fi/evals/framework/evals/builder.py +609 -0
- fi/evals/framework/evals/semantic.py +142 -0
- fi/evals/framework/evaluator.py +647 -0
- fi/evals/framework/evaluators/__init__.py +22 -0
- fi/evals/framework/evaluators/blocking.py +347 -0
- fi/evals/framework/evaluators/non_blocking.py +577 -0
- fi/evals/framework/propagation.py +421 -0
- fi/evals/framework/protocols.py +385 -0
- fi/evals/framework/registry.py +370 -0
- fi/evals/framework/resilience/__init__.py +150 -0
- fi/evals/framework/resilience/circuit_breaker.py +309 -0
- fi/evals/framework/resilience/degradation.py +355 -0
- fi/evals/framework/resilience/health.py +505 -0
- fi/evals/framework/resilience/rate_limiter.py +228 -0
- fi/evals/framework/resilience/retry.py +274 -0
- fi/evals/framework/resilience/types.py +288 -0
- fi/evals/framework/resilience/wrapper.py +433 -0
- fi/evals/framework/types.py +218 -0
- fi/evals/guardrails/README.md +915 -0
- fi/evals/guardrails/__init__.py +96 -0
- fi/evals/guardrails/backends/__init__.py +43 -0
- fi/evals/guardrails/backends/azure.py +361 -0
- fi/evals/guardrails/backends/base.py +88 -0
- fi/evals/guardrails/backends/generic_llm.py +163 -0
- fi/evals/guardrails/backends/granite.py +216 -0
- fi/evals/guardrails/backends/llamaguard.py +221 -0
- fi/evals/guardrails/backends/local_base.py +479 -0
- fi/evals/guardrails/backends/openai.py +365 -0
- fi/evals/guardrails/backends/qwen.py +170 -0
- fi/evals/guardrails/backends/shieldgemma.py +154 -0
- fi/evals/guardrails/backends/turing.py +235 -0
- fi/evals/guardrails/backends/vllm_client.py +321 -0
- fi/evals/guardrails/backends/wildguard.py +188 -0
- fi/evals/guardrails/base.py +888 -0
- fi/evals/guardrails/config.py +221 -0
- fi/evals/guardrails/discovery.py +243 -0
- fi/evals/guardrails/gateway.py +437 -0
- fi/evals/guardrails/registry.py +231 -0
- fi/evals/guardrails/scanners/__init__.py +127 -0
- fi/evals/guardrails/scanners/base.py +191 -0
- fi/evals/guardrails/scanners/code_injection.py +243 -0
- fi/evals/guardrails/scanners/eval_delegate.py +574 -0
- fi/evals/guardrails/scanners/invisible_chars.py +351 -0
- fi/evals/guardrails/scanners/jailbreak.py +412 -0
- fi/evals/guardrails/scanners/language.py +288 -0
- fi/evals/guardrails/scanners/pipeline.py +260 -0
- fi/evals/guardrails/scanners/regex.py +311 -0
- fi/evals/guardrails/scanners/secrets.py +274 -0
- fi/evals/guardrails/scanners/topics.py +649 -0
- fi/evals/guardrails/scanners/urls.py +341 -0
- fi/evals/guardrails/types.py +96 -0
- fi/evals/llm/__init__.py +3 -0
- fi/evals/llm/base_llm_provider.py +35 -0
- fi/evals/llm/providers/litellm.py +70 -0
- fi/evals/local/__init__.py +90 -0
- fi/evals/local/evaluator.py +690 -0
- fi/evals/local/execution_mode.py +121 -0
- fi/evals/local/llm.py +489 -0
- fi/evals/local/metrics/__init__.py +19 -0
- fi/evals/local/registry.py +360 -0
- fi/evals/manager.py +1018 -0
- fi/evals/manager_types.py +362 -0
- fi/evals/metrics/__init__.py +185 -0
- fi/evals/metrics/agents/__init__.py +74 -0
- fi/evals/metrics/agents/metrics.py +693 -0
- fi/evals/metrics/agents/report.py +36463 -0
- fi/evals/metrics/agents/types.py +160 -0
- fi/evals/metrics/base_llm_metric.py +111 -0
- fi/evals/metrics/base_metric.py +138 -0
- fi/evals/metrics/code_security/__init__.py +305 -0
- fi/evals/metrics/code_security/analyzer.py +985 -0
- fi/evals/metrics/code_security/benchmarks/__init__.py +73 -0
- fi/evals/metrics/code_security/benchmarks/builtin.py +750 -0
- fi/evals/metrics/code_security/benchmarks/loader.py +580 -0
- fi/evals/metrics/code_security/benchmarks/types.py +308 -0
- fi/evals/metrics/code_security/detectors/__init__.py +186 -0
- fi/evals/metrics/code_security/detectors/base.py +394 -0
- fi/evals/metrics/code_security/detectors/cryptography.py +345 -0
- fi/evals/metrics/code_security/detectors/injection.py +744 -0
- fi/evals/metrics/code_security/detectors/secrets.py +287 -0
- fi/evals/metrics/code_security/detectors/serialization.py +192 -0
- fi/evals/metrics/code_security/joint_metrics.py +588 -0
- fi/evals/metrics/code_security/judges/__init__.py +83 -0
- fi/evals/metrics/code_security/judges/base.py +238 -0
- fi/evals/metrics/code_security/judges/dual_judge.py +534 -0
- fi/evals/metrics/code_security/judges/llm_judge.py +301 -0
- fi/evals/metrics/code_security/judges/pattern_judge.py +515 -0
- fi/evals/metrics/code_security/metrics.py +388 -0
- fi/evals/metrics/code_security/modes/__init__.py +63 -0
- fi/evals/metrics/code_security/modes/adversarial.py +284 -0
- fi/evals/metrics/code_security/modes/autocomplete.py +198 -0
- fi/evals/metrics/code_security/modes/base.py +283 -0
- fi/evals/metrics/code_security/modes/instruct.py +253 -0
- fi/evals/metrics/code_security/modes/repair.py +230 -0
- fi/evals/metrics/code_security/reports/__init__.py +57 -0
- fi/evals/metrics/code_security/reports/generator.py +404 -0
- fi/evals/metrics/code_security/reports/leaderboard.py +509 -0
- fi/evals/metrics/code_security/types.py +534 -0
- fi/evals/metrics/function_calling/__init__.py +34 -0
- fi/evals/metrics/function_calling/metrics.py +573 -0
- fi/evals/metrics/function_calling/types.py +87 -0
- fi/evals/metrics/hallucination/__init__.py +54 -0
- fi/evals/metrics/hallucination/detector.py +149 -0
- fi/evals/metrics/hallucination/metrics.py +390 -0
- fi/evals/metrics/hallucination/nli.py +253 -0
- fi/evals/metrics/hallucination/sentinel.py +106 -0
- fi/evals/metrics/hallucination/types.py +132 -0
- fi/evals/metrics/heuristics/aggregation_metrics.py +85 -0
- fi/evals/metrics/heuristics/json_metrics.py +87 -0
- fi/evals/metrics/heuristics/similarity_metrics.py +375 -0
- fi/evals/metrics/heuristics/string_metrics.py +391 -0
- fi/evals/metrics/llm_as_judges/__init__.py +17 -0
- fi/evals/metrics/llm_as_judges/custom_judge/metric.py +112 -0
- fi/evals/metrics/llm_as_judges/custom_judge/prompts.py +26 -0
- fi/evals/metrics/llm_as_judges/types.py +48 -0
- fi/evals/metrics/rag/__init__.py +111 -0
- fi/evals/metrics/rag/advanced/__init__.py +14 -0
- fi/evals/metrics/rag/advanced/multi_hop.py +283 -0
- fi/evals/metrics/rag/advanced/source_attribution.py +344 -0
- fi/evals/metrics/rag/generation/__init__.py +17 -0
- fi/evals/metrics/rag/generation/answer_relevancy.py +176 -0
- fi/evals/metrics/rag/generation/context_utilization.py +245 -0
- fi/evals/metrics/rag/generation/faithfulness.py +241 -0
- fi/evals/metrics/rag/generation/groundedness.py +131 -0
- fi/evals/metrics/rag/rag_score.py +277 -0
- fi/evals/metrics/rag/retrieval/__init__.py +20 -0
- fi/evals/metrics/rag/retrieval/context_entity_recall.py +124 -0
- fi/evals/metrics/rag/retrieval/context_precision.py +158 -0
- fi/evals/metrics/rag/retrieval/context_recall.py +106 -0
- fi/evals/metrics/rag/retrieval/noise_sensitivity.py +163 -0
- fi/evals/metrics/rag/retrieval/ranking.py +261 -0
- fi/evals/metrics/rag/types.py +100 -0
- fi/evals/metrics/rag/utils/__init__.py +62 -0
- fi/evals/metrics/rag/utils/claims.py +189 -0
- fi/evals/metrics/rag/utils/entities.py +244 -0
- fi/evals/metrics/rag/utils/nli.py +92 -0
- fi/evals/metrics/rag/utils/similarity.py +345 -0
- fi/evals/metrics/structured/__init__.py +114 -0
- fi/evals/metrics/structured/field_completeness.py +313 -0
- fi/evals/metrics/structured/hierarchy_score.py +366 -0
- fi/evals/metrics/structured/json_validation.py +190 -0
- fi/evals/metrics/structured/schema_compliance.py +280 -0
- fi/evals/metrics/structured/structured_output_score.py +298 -0
- fi/evals/metrics/structured/types.py +108 -0
- fi/evals/metrics/structured/validators/__init__.py +30 -0
- fi/evals/metrics/structured/validators/base.py +189 -0
- fi/evals/metrics/structured/validators/json_validator.py +196 -0
- fi/evals/metrics/structured/validators/pydantic_validator.py +178 -0
- fi/evals/metrics/structured/validators/yaml_validator.py +248 -0
- fi/evals/otel/__init__.py +266 -0
- fi/evals/otel/config.py +400 -0
- fi/evals/otel/conventions.py +463 -0
- fi/evals/otel/enrichment.py +371 -0
- fi/evals/otel/instrumentors/__init__.py +140 -0
- fi/evals/otel/instrumentors/anthropic.py +517 -0
- fi/evals/otel/instrumentors/base.py +382 -0
- fi/evals/otel/instrumentors/openai.py +673 -0
- fi/evals/otel/processors/__init__.py +36 -0
- fi/evals/otel/processors/base.py +473 -0
- fi/evals/otel/processors/cost.py +445 -0
- fi/evals/otel/processors/evaluation.py +559 -0
- fi/evals/otel/processors/llm.py +462 -0
- fi/evals/otel/tracer.py +506 -0
- fi/evals/otel/types.py +232 -0
- fi/evals/otel_utils.py +23 -0
- fi/evals/protect.py +671 -0
- fi/evals/protect_input_adapter.py +154 -0
- fi/evals/streaming/__init__.py +88 -0
- fi/evals/streaming/buffer.py +213 -0
- fi/evals/streaming/evaluator.py +551 -0
- fi/evals/streaming/policy.py +307 -0
- fi/evals/streaming/scorers.py +368 -0
- fi/evals/streaming/types.py +238 -0
- fi/evals/templates.py +472 -0
- fi/evals/types.py +156 -0
- fi/opt/__init__.py +221 -0
- fi/opt/_objective_scoring.py +85 -0
- fi/opt/base/__init__.py +11 -0
- fi/opt/base/base_generator.py +33 -0
- fi/opt/base/base_mapper.py +26 -0
- fi/opt/base/base_optimizer.py +45 -0
- fi/opt/base/evaluator.py +211 -0
- fi/opt/components.py +3095 -0
- fi/opt/datamappers/__init__.py +3 -0
- fi/opt/datamappers/basic_mapper.py +40 -0
- fi/opt/deployment.py +1021 -0
- fi/opt/evidence.py +4332 -0
- fi/opt/generators/__init__.py +3 -0
- fi/opt/generators/litellm.py +66 -0
- fi/opt/integrations/__init__.py +23 -0
- fi/opt/integrations/generative_suite.py +410 -0
- fi/opt/integrations/simulate.py +1313 -0
- fi/opt/mutations.py +771 -0
- fi/opt/observability.py +4639 -0
- fi/opt/optimizer_trace.py +889 -0
- fi/opt/optimizers/__init__.py +80 -0
- fi/opt/optimizers/agent.py +331 -0
- fi/opt/optimizers/agent_bandit.py +392 -0
- fi/opt/optimizers/agent_curriculum.py +635 -0
- fi/opt/optimizers/agent_evolution.py +894 -0
- fi/opt/optimizers/agent_feedback.py +1863 -0
- fi/opt/optimizers/agent_pareto.py +547 -0
- fi/opt/optimizers/agent_social_memory.py +1113 -0
- fi/opt/optimizers/agent_tpe.py +321 -0
- fi/opt/optimizers/bayesian_search.py +449 -0
- fi/opt/optimizers/council.py +2075 -0
- fi/opt/optimizers/futureagi_replay.py +799 -0
- fi/opt/optimizers/gepa.py +322 -0
- fi/opt/optimizers/metaprompt.py +243 -0
- fi/opt/optimizers/promptwizard.py +417 -0
- fi/opt/optimizers/protegi.py +329 -0
- fi/opt/optimizers/random_search.py +224 -0
- fi/opt/research.py +518 -0
- fi/opt/simulation.py +260 -0
- fi/opt/targets.py +232 -0
- fi/opt/types.py +66 -0
- fi/opt/utils/__init__.py +4 -0
- fi/opt/utils/early_stopping.py +266 -0
- fi/opt/utils/setup_logging.py +82 -0
- fi/simulate/__init__.py +540 -0
- fi/simulate/_hashing.py +35 -0
- fi/simulate/_logging.py +10 -0
- fi/simulate/adapters.py +87 -0
- fi/simulate/agent/__init__.py +120 -0
- fi/simulate/agent/browser.py +658 -0
- fi/simulate/agent/definition.py +587 -0
- fi/simulate/agent/frameworks.py +3528 -0
- fi/simulate/agent/generic.py +8286 -0
- fi/simulate/agent/import_probe.py +227 -0
- fi/simulate/agent/memory.py +905 -0
- fi/simulate/agent/mocks.py +101 -0
- fi/simulate/agent/multi_agent.py +361 -0
- fi/simulate/agent/orchestration.py +903 -0
- fi/simulate/agent/realtime.py +665 -0
- fi/simulate/agent/wrapper.py +99 -0
- fi/simulate/agent/wrappers/__init__.py +18 -0
- fi/simulate/agent/wrappers/anthropic.py +62 -0
- fi/simulate/agent/wrappers/gemini.py +65 -0
- fi/simulate/agent/wrappers/http.py +404 -0
- fi/simulate/agent/wrappers/langchain.py +80 -0
- fi/simulate/agent/wrappers/openai.py +75 -0
- fi/simulate/agent/wrappers/websocket.py +326 -0
- fi/simulate/artifacts/__init__.py +11 -0
- fi/simulate/artifacts/manifest.py +62 -0
- fi/simulate/cli.py +20560 -0
- fi/simulate/endpoints/__init__.py +45 -0
- fi/simulate/endpoints/_http_actor.py +73 -0
- fi/simulate/endpoints/actor_sources.py +243 -0
- fi/simulate/endpoints/base.py +107 -0
- fi/simulate/endpoints/builtins.py +10 -0
- fi/simulate/endpoints/callable.py +95 -0
- fi/simulate/endpoints/http.py +76 -0
- fi/simulate/endpoints/livekit.py +138 -0
- fi/simulate/endpoints/originators.py +132 -0
- fi/simulate/endpoints/profiles.py +348 -0
- fi/simulate/endpoints/retell.py +633 -0
- fi/simulate/endpoints/vapi.py +205 -0
- fi/simulate/endpoints/websocket.py +76 -0
- fi/simulate/environment.py +33026 -0
- fi/simulate/environments/__init__.py +11 -0
- fi/simulate/environments/base.py +73 -0
- fi/simulate/environments/chat.py +697 -0
- fi/simulate/environments/voice.py +212 -0
- fi/simulate/evaluation/__init__.py +4 -0
- fi/simulate/evaluation/ai_eval.py +227 -0
- fi/simulate/evidence/__init__.py +35 -0
- fi/simulate/evidence/base.py +59 -0
- fi/simulate/evidence/caller_observed.py +50 -0
- fi/simulate/evidence/livekit_instrumentation.py +51 -0
- fi/simulate/evidence/livekit_room.py +50 -0
- fi/simulate/evidence/otel.py +49 -0
- fi/simulate/evidence/providers/__init__.py +24 -0
- fi/simulate/evidence/providers/base.py +61 -0
- fi/simulate/evidence/providers/retell.py +376 -0
- fi/simulate/evidence/providers/vapi.py +426 -0
- fi/simulate/hosted/__init__.py +32 -0
- fi/simulate/hosted/child_entrypoint.py +306 -0
- fi/simulate/hosted/job.py +150 -0
- fi/simulate/hosted/targets.py +53 -0
- fi/simulate/instrumentation/__init__.py +5 -0
- fi/simulate/instrumentation/livekit/__init__.py +122 -0
- fi/simulate/manifest.py +1033 -0
- fi/simulate/matrix_cli.py +165 -0
- fi/simulate/realtime/__init__.py +40 -0
- fi/simulate/realtime/events.py +107 -0
- fi/simulate/realtime/media.py +61 -0
- fi/simulate/realtime/session.py +91 -0
- fi/simulate/recording/__init__.py +5 -0
- fi/simulate/recording/room_recorder.py +326 -0
- fi/simulate/registry.py +185 -0
- fi/simulate/results/__init__.py +9 -0
- fi/simulate/results/base.py +18 -0
- fi/simulate/results/filesystem.py +71 -0
- fi/simulate/results/futureagi.py +1340 -0
- fi/simulate/runtime/__init__.py +85 -0
- fi/simulate/runtime/capabilities.py +40 -0
- fi/simulate/runtime/events.py +63 -0
- fi/simulate/runtime/failures.py +25 -0
- fi/simulate/runtime/ids.py +34 -0
- fi/simulate/runtime/plan.py +70 -0
- fi/simulate/runtime/planner.py +102 -0
- fi/simulate/runtime/report.py +174 -0
- fi/simulate/runtime/run.py +75 -0
- fi/simulate/runtime/runner.py +333 -0
- fi/simulate/runtime/spec.py +186 -0
- fi/simulate/simulation/__init__.py +30 -0
- fi/simulate/simulation/behavior_policy.py +425 -0
- fi/simulate/simulation/bridge/__init__.py +9 -0
- fi/simulate/simulation/bridge/audio.py +29 -0
- fi/simulate/simulation/bridge/connector.py +46 -0
- fi/simulate/simulation/bridge/livekit.py +252 -0
- fi/simulate/simulation/bridge/retell.py +188 -0
- fi/simulate/simulation/bridge/vapi.py +177 -0
- fi/simulate/simulation/contract.py +419 -0
- fi/simulate/simulation/engines/__init__.py +12 -0
- fi/simulate/simulation/engines/base.py +21 -0
- fi/simulate/simulation/engines/cloud.py +517 -0
- fi/simulate/simulation/engines/livekit.py +4167 -0
- fi/simulate/simulation/engines/local_text.py +89 -0
- fi/simulate/simulation/fidelity.py +374 -0
- fi/simulate/simulation/gemini_tts_stream.py +110 -0
- fi/simulate/simulation/generator.py +91 -0
- fi/simulate/simulation/goal_machine.py +185 -0
- fi/simulate/simulation/livekit_models.py +467 -0
- fi/simulate/simulation/matrix.py +170 -0
- fi/simulate/simulation/models.py +279 -0
- fi/simulate/simulation/runner.py +153 -0
- fi/simulate/simulation/synthetic.py +880 -0
- fi/simulate/simulation/voice_prompt.py +502 -0
- fi/simulate/simulator/__init__.py +55 -0
- fi/simulate/simulator/builtins.py +53 -0
- fi/simulate/suite.py +1288 -0
- fi/simulate/utils/routes.py +164 -0
- fi/simulate/voice.py +225 -0
- fi/simulate/voice_cli.py +182 -0
- fi/utils/__init__.py +1 -0
- fi/utils/constants.py +14 -0
- fi/utils/errors.py +200 -0
- fi/utils/executor.py +26 -0
- fi/utils/routes.py +119 -0
- fi/utils/utils.py +17 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
"""Caller voice and behaviour settings shared by the local and hosted lanes.
|
|
2
|
+
|
|
3
|
+
Both lanes build the same simulated customer. Keeping the rules and the provider choice here
|
|
4
|
+
means a change lands in both rather than in whichever one the author had open.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable, Mapping
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from fi import simulate
|
|
17
|
+
from fi.simulate.runtime import (
|
|
18
|
+
AgentEndpointSpec,
|
|
19
|
+
EnvironmentSpec,
|
|
20
|
+
ExecutionPolicy,
|
|
21
|
+
SimulationSpec,
|
|
22
|
+
SimulatorPolicySpec,
|
|
23
|
+
TimeoutPolicy,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
CARTESIA_DEFAULT_VOICE = "f786b574-daa5-4673-aa0c-cbe3e8534c02"
|
|
29
|
+
|
|
30
|
+
CONNECT_TIMEOUT_SECONDS = 60.0
|
|
31
|
+
READINESS_TIMEOUT_SECONDS = 120.0
|
|
32
|
+
CLEANUP_TIMEOUT_SECONDS = 30.0
|
|
33
|
+
|
|
34
|
+
_TARGET_NAME = "harness-livekit-target"
|
|
35
|
+
_BEHAVIOR_POLICY = {
|
|
36
|
+
"disclosure_policy": 0.72,
|
|
37
|
+
"cooperation_bounds": 0.9,
|
|
38
|
+
"repair_propensity": 0.85,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Languages transcribed with Deepgram's multilingual model rather than a single language code.
|
|
42
|
+
_MULTILINGUAL_STT = ("ar", "es")
|
|
43
|
+
|
|
44
|
+
# Written as separate numbered rules rather than one paragraph. These arrive late in a long
|
|
45
|
+
# prompt, and a rule buried mid-sentence there does not survive: a caller ignored the loop rule
|
|
46
|
+
# for four turns while it was the tail of a compound sentence.
|
|
47
|
+
SIMULATOR_INSTRUCTIONS = (
|
|
48
|
+
"Act as the customer described by the scenario. Speak naturally and briefly.\n"
|
|
49
|
+
"These rules override anything else when they conflict:\n"
|
|
50
|
+
"1. Use ONLY the facts you were given. Never invent an account detail, address, "
|
|
51
|
+
"payment state, or verification code.\n"
|
|
52
|
+
"2. If the agent asks something ordinary you were given no fact for, your age, your job, why "
|
|
53
|
+
"you need this, roughly when something happened, a nearby landmark, answer the way a real "
|
|
54
|
+
"person would: give a plausible answer that fits who you are, and keep it consistent for the "
|
|
55
|
+
"rest of the call. Say you do not know only where a real person would not know.\n"
|
|
56
|
+
"2b. Anything the agent checks against its own records is different: an account number, a "
|
|
57
|
+
"booking reference, a verification code, what you were charged, what it has on file. If you "
|
|
58
|
+
"were not given it, say you do not have it to hand. Never make one up, because an invented one "
|
|
59
|
+
"is checked, fails, and tells nobody anything.\n"
|
|
60
|
+
"2c. Anything you do make up has to sound like a real person's rather than a placeholder. Not "
|
|
61
|
+
"a run of digits for a phone number, not a round demo amount, not a birthday of 01/01/2000. "
|
|
62
|
+
"Fit it to where you live and how old you are.\n"
|
|
63
|
+
"3. Answer only what was asked, one fact at a time. Do not volunteer anything the "
|
|
64
|
+
"agent has not asked for and do not offer several details at once to be helpful, even "
|
|
65
|
+
"when you know they will be needed next. Agree when asked whether a verification code "
|
|
66
|
+
"should be sent, and read the code out only after the agent says it was sent and "
|
|
67
|
+
"asks you for it. This governs FACTS about you, your account and your situation. It does "
|
|
68
|
+
"not stop you asking your own question about the call itself, such as how long this will "
|
|
69
|
+
"take or why something is needed; that is not volunteering, and rules 12a to 12d say when "
|
|
70
|
+
"to do it.\n"
|
|
71
|
+
"4. Answer a repair question with the missing fact, not by restarting your request.\n"
|
|
72
|
+
"5. STOP AFTER THREE. Count the agent's replies. If three of them say essentially "
|
|
73
|
+
"the same thing without the task moving forward, do not try a fifth time and do not "
|
|
74
|
+
"rephrase the same point again. Say once that this is not working and you will try "
|
|
75
|
+
"later, then end the call.\n"
|
|
76
|
+
"6. Otherwise let the agent finish speaking. Never start a reply from a partial sentence "
|
|
77
|
+
"or while the agent is reading a summary. Wait for the complete question before answering.\n"
|
|
78
|
+
"7. A quote, proposed action, or booking summary is not a completed outcome. If the agent "
|
|
79
|
+
"asks for final confirmation, answer explicitly, then remain on the call until the agent "
|
|
80
|
+
"confirms that the action actually completed. Do not use goodbye or other closing language "
|
|
81
|
+
"before that confirmation.\n"
|
|
82
|
+
"8. Follow sequence words literally. If the scenario says to do something after an earlier "
|
|
83
|
+
"action is completed, do not reveal or request the later action in the same reply that "
|
|
84
|
+
"confirms the earlier one. Wait until the agent explicitly confirms the earlier action.\n"
|
|
85
|
+
"9. Once the outcome is confirmed, close in ONE turn and end the call. EVERYTHING you still "
|
|
86
|
+
"have to say goes inside that turn: a thanks, a last condition, a reminder, a warning, a "
|
|
87
|
+
"caveat. 'Alright, make sure it stays off the list. Goodbye.' is one closing; 'Goodbye.' "
|
|
88
|
+
"followed by 'Make sure it stays off the list.' is two, and the second one is the tell. Say "
|
|
89
|
+
"your last point BEFORE the farewell, in the same breath, or do not say it at all.\n"
|
|
90
|
+
"10. After your closing turn you say nothing further, whatever the agent says next. Do not "
|
|
91
|
+
"apologise, do not thank the agent more than once, do not trade thanks back and forth, and "
|
|
92
|
+
"do not answer a goodbye with another goodbye.\n"
|
|
93
|
+
"11. Say where you are or what you are doing only if the agent asks or it genuinely matters. It "
|
|
94
|
+
"is background, not something to announce.\n"
|
|
95
|
+
"12. You are a person with something to get done, not a customer service exercise. Perfect "
|
|
96
|
+
"politeness through a call that is going badly is how a machine talks, and it makes the test "
|
|
97
|
+
"worthless: nobody learns anything from an agent that was never pushed. This applies just as "
|
|
98
|
+
"much when the call is going FINE, which is most of the time: a person who is being helped "
|
|
99
|
+
"competently still reacts, still wonders, still gets tired of question fifteen of twenty.\n"
|
|
100
|
+
"12a. React to what you are told, not only to what you are asked. A figure that sounds high, a "
|
|
101
|
+
"wait that sounds long, a step that sounds pointless: say so the first time you hear it, in "
|
|
102
|
+
"your own words, once. Ask why something is needed where a person would genuinely wonder.\n"
|
|
103
|
+
"12b. Gratitude is not punctuation. Do not open a turn with thanks, do not use 'please' as "
|
|
104
|
+
"filler on a plain answer, and never say 'thank you so much', 'I really appreciate it' or "
|
|
105
|
+
"'sorry to bother you'. Answering a question is not a favour done to you, and a stream of "
|
|
106
|
+
"courtesies is the clearest sign in a transcript that nobody real was on the line.\n"
|
|
107
|
+
"12c. If you are asked something you have already answered, say that you already gave it, "
|
|
108
|
+
"once, and then give it again. Answering it twice as though it were new is the clearest sign "
|
|
109
|
+
"nobody is really listening on your side either.\n"
|
|
110
|
+
"12d. Your patience for being asked question after question is finite. Once you have answered "
|
|
111
|
+
"roughly ten in a row, say ONCE that you would like to know how many more there are, or how "
|
|
112
|
+
"long this will take, or that you have somewhere to be. Then carry on answering. This is not "
|
|
113
|
+
"refusing to co-operate: it is the single most common thing a real person does on a long form, "
|
|
114
|
+
"and a caller who never does it turns a twenty-minute intake into a transcript nobody can "
|
|
115
|
+
"learn anything from.\n"
|
|
116
|
+
"13. Never say you have done something away from this call that you cannot actually do: "
|
|
117
|
+
"tapped a link, opened an app, read a message that arrived, paid something elsewhere. You are "
|
|
118
|
+
"on a phone call and nothing else. Say plainly that nothing has arrived or that you cannot do "
|
|
119
|
+
"it, and let the agent find another way. Claiming it leaves the agent waiting for a change "
|
|
120
|
+
"that never happens, and the call goes nowhere for both of you."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# An outbound call is not an inbound call with the greeting reworded. The person did not dial in,
|
|
124
|
+
# so they have no opening request to make and no reason to explain themselves first. A caller who
|
|
125
|
+
# states their task anyway tests nothing about how the agent opens a call it placed.
|
|
126
|
+
_OUTBOUND_FRAMING = (
|
|
127
|
+
"\nTHIS CALL WAS PLACED TO YOU. You did not dial anyone. You were doing something else when "
|
|
128
|
+
"the phone rang. These override the numbered rules wherever they disagree.\n"
|
|
129
|
+
"A. Answer the way anyone answers a ringing phone: a short hello, nothing more. Do not state a "
|
|
130
|
+
"reason for calling, because you have none.\n"
|
|
131
|
+
"B. Let them say who they are and what they want. Until they do you have nothing to go on, so "
|
|
132
|
+
"do not guess at it or help them along.\n"
|
|
133
|
+
"C. You have no errand of your own here. You are not trying to get anything done; you are "
|
|
134
|
+
"deciding whether to give this person your time and answering what they ask.\n"
|
|
135
|
+
"D. Never supply an account detail, address or code before they have explained why they "
|
|
136
|
+
"called. An unexpected call asking for those is what a scam sounds like, so asking them to "
|
|
137
|
+
"prove themselves first is correct, not obstruction. It is also reasonable to ask how long "
|
|
138
|
+
"this will take, or to say it is a bad moment.\n"
|
|
139
|
+
"E. End the call when they have finished with you, not when you have got what you came for, "
|
|
140
|
+
"because you came for nothing.\n"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# How much this person already knows about why they are being called. Each one is a different test:
|
|
144
|
+
# the first checks the agent can proceed, the last checks it can establish context first.
|
|
145
|
+
_OUTBOUND_AWARENESS = {
|
|
146
|
+
"expecting": (
|
|
147
|
+
"F. You were told to expect this call and roughly what it concerns. Once they identify "
|
|
148
|
+
"themselves, cooperate normally.\n"
|
|
149
|
+
),
|
|
150
|
+
"partial": (
|
|
151
|
+
"F. You half remember arranging something like this and not the details. Say so plainly "
|
|
152
|
+
"rather than inventing the specifics you were not given.\n"
|
|
153
|
+
),
|
|
154
|
+
"unaware": (
|
|
155
|
+
"F. You do not know why anyone would be calling you. Ask what this is about and stay "
|
|
156
|
+
"slightly guarded until they have explained themselves. You still answer ordinary "
|
|
157
|
+
"questions about yourself once they have.\n"
|
|
158
|
+
),
|
|
159
|
+
}
|
|
160
|
+
_DEFAULT_OUTBOUND_AWARENESS = "unaware"
|
|
161
|
+
|
|
162
|
+
# Replaces the caller's rules rather than adding to them: every rule above assumes a listener.
|
|
163
|
+
_VOICEMAIL_INSTRUCTIONS = (
|
|
164
|
+
"YOU ARE A VOICEMAIL SYSTEM, not a person. This call was placed to a number whose owner did "
|
|
165
|
+
"not pick up, and you are the mailbox that answered instead.\n"
|
|
166
|
+
"1. Say your greeting once, at the very start, and nothing else for the rest of the call.\n"
|
|
167
|
+
"2. After the greeting you are silent. Whatever the caller says, whatever they ask, however "
|
|
168
|
+
"many times they ask it, you do not reply. You are a recording being played, not a listener.\n"
|
|
169
|
+
"3. Never answer a question, never confirm or deny anything, never give any detail, never say "
|
|
170
|
+
"yes or no, and never repeat the greeting.\n"
|
|
171
|
+
"4. Never end the call. A mailbox records until the caller hangs up or the line is cut.\n"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Only the greeting differs. "full" must never invite a message it cannot take, and where a
|
|
175
|
+
# recording greets, the session must not speak at all.
|
|
176
|
+
_VOICEMAIL_RECORDED = (
|
|
177
|
+
"YOU ARE A VOICEMAIL SYSTEM and your greeting is a recording that is already playing. Say "
|
|
178
|
+
"NOTHING for the whole call. Not a greeting, not a word, not a sound, whatever the caller says "
|
|
179
|
+
"or asks or how many times they ask it. There is no turn for you to take. Never end the call "
|
|
180
|
+
"either; a mailbox records until the caller hangs up.\n"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
_VOICEMAIL_BY_STYLE = {
|
|
185
|
+
"personal": "5. Your greeting is your own, recorded in your own words: say who you are, that "
|
|
186
|
+
"you cannot take the call, and to leave a message. Keep it to a sentence or two.\n",
|
|
187
|
+
"carrier": "5. Your greeting is the network's default and names nobody at all. Say that the "
|
|
188
|
+
"person called is not available and to record a message after the tone. Never give a name, "
|
|
189
|
+
"not even if the caller asks for one.\n",
|
|
190
|
+
"operator": "5. Your greeting is a formal automated announcement, longer and more stilted than "
|
|
191
|
+
"a person would record: say the call has been forwarded to an automated voice messaging "
|
|
192
|
+
"system, that the subscriber is unavailable, and that a message may be recorded at the tone. "
|
|
193
|
+
"Name nobody.\n",
|
|
194
|
+
"full": "5. This mailbox is FULL. Say that it cannot accept any new messages, that the caller "
|
|
195
|
+
"should try again later, and end the greeting there. Never invite a message and never mention "
|
|
196
|
+
"a tone, because there is no tone and nothing will be recorded.\n",
|
|
197
|
+
}
|
|
198
|
+
_DEFAULT_VOICEMAIL_STYLE = "personal"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def simulator_instructions(
|
|
202
|
+
direction: str = "",
|
|
203
|
+
awareness: str = "",
|
|
204
|
+
answered_by: str = "",
|
|
205
|
+
voicemail_style: str = "",
|
|
206
|
+
recorded: bool = False,
|
|
207
|
+
) -> str:
|
|
208
|
+
"""The caller's rules, framed by whether this call was placed to them or by them.
|
|
209
|
+
|
|
210
|
+
Chat has no direction: a chat is always started by the person, so it takes the inbound text.
|
|
211
|
+
A mailbox answering replaces the rules outright, because it is not a person.
|
|
212
|
+
"""
|
|
213
|
+
if str(answered_by).strip().lower() == "voicemail":
|
|
214
|
+
style = str(voicemail_style).strip().lower() or _DEFAULT_VOICEMAIL_STYLE
|
|
215
|
+
if recorded:
|
|
216
|
+
return _VOICEMAIL_RECORDED
|
|
217
|
+
return _VOICEMAIL_INSTRUCTIONS + _VOICEMAIL_BY_STYLE.get(
|
|
218
|
+
style, _VOICEMAIL_BY_STYLE[_DEFAULT_VOICEMAIL_STYLE]
|
|
219
|
+
)
|
|
220
|
+
if str(direction).strip().lower() != "outbound":
|
|
221
|
+
return SIMULATOR_INSTRUCTIONS
|
|
222
|
+
chosen = str(awareness).strip().lower() or _DEFAULT_OUTBOUND_AWARENESS
|
|
223
|
+
return (
|
|
224
|
+
SIMULATOR_INSTRUCTIONS
|
|
225
|
+
+ _OUTBOUND_FRAMING
|
|
226
|
+
+ _OUTBOUND_AWARENESS.get(chosen, _OUTBOUND_AWARENESS[_DEFAULT_OUTBOUND_AWARENESS])
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
_LANGUAGE_CODES: dict[str, str] = {
|
|
230
|
+
"ar": "ar",
|
|
231
|
+
"ar-sa": "ar",
|
|
232
|
+
"arabic": "ar",
|
|
233
|
+
"bg": "bg",
|
|
234
|
+
"bulgarian": "bg",
|
|
235
|
+
"ca": "ca",
|
|
236
|
+
"catalan": "ca",
|
|
237
|
+
"chinese": "zh",
|
|
238
|
+
"chinese simplified": "zh",
|
|
239
|
+
"chinese traditional": "zh-TW",
|
|
240
|
+
"chinese (cantonese, traditional)": "zh-HK",
|
|
241
|
+
"chinese (mandarin, simplified)": "zh",
|
|
242
|
+
"chinese (mandarin, traditional)": "zh-TW",
|
|
243
|
+
"cs": "cs",
|
|
244
|
+
"czech": "cs",
|
|
245
|
+
"da": "da",
|
|
246
|
+
"da-dk": "da",
|
|
247
|
+
"danish": "da",
|
|
248
|
+
"de": "de",
|
|
249
|
+
"de-ch": "de-CH",
|
|
250
|
+
"dutch": "nl",
|
|
251
|
+
"el": "el",
|
|
252
|
+
"en": "en-US",
|
|
253
|
+
"en-au": "en-AU",
|
|
254
|
+
"en-gb": "en-GB",
|
|
255
|
+
"en-in": "en-IN",
|
|
256
|
+
"en-nz": "en-NZ",
|
|
257
|
+
"en-us": "en-US",
|
|
258
|
+
"english": "en-US",
|
|
259
|
+
"es": "es",
|
|
260
|
+
"es-419": "es-419",
|
|
261
|
+
"estonian": "et",
|
|
262
|
+
"et": "et",
|
|
263
|
+
"fi": "fi",
|
|
264
|
+
"finnish": "fi",
|
|
265
|
+
"flemish": "nl-BE",
|
|
266
|
+
"fr": "fr",
|
|
267
|
+
"fr-ca": "fr-CA",
|
|
268
|
+
"french": "fr",
|
|
269
|
+
"german": "de",
|
|
270
|
+
"greek": "el",
|
|
271
|
+
"hi": "hi",
|
|
272
|
+
"hindi": "hi",
|
|
273
|
+
"hu": "hu",
|
|
274
|
+
"hungarian": "hu",
|
|
275
|
+
"id": "id",
|
|
276
|
+
"indonesian": "id",
|
|
277
|
+
"it": "it",
|
|
278
|
+
"italian": "it",
|
|
279
|
+
"ja": "ja",
|
|
280
|
+
"japanese": "ja",
|
|
281
|
+
"ko": "ko",
|
|
282
|
+
"ko-kr": "ko",
|
|
283
|
+
"korean": "ko",
|
|
284
|
+
"latvian": "lv",
|
|
285
|
+
"lithuanian": "lt",
|
|
286
|
+
"lt": "lt",
|
|
287
|
+
"lv": "lv",
|
|
288
|
+
"malay": "ms",
|
|
289
|
+
"ms": "ms",
|
|
290
|
+
"nl": "nl",
|
|
291
|
+
"nl-be": "nl-BE",
|
|
292
|
+
"no": "no",
|
|
293
|
+
"norwegian": "no",
|
|
294
|
+
"pl": "pl",
|
|
295
|
+
"polish": "pl",
|
|
296
|
+
"portuguese": "pt",
|
|
297
|
+
"pt": "pt",
|
|
298
|
+
"pt-br": "pt-BR",
|
|
299
|
+
"pt-pt": "pt-PT",
|
|
300
|
+
"ro": "ro",
|
|
301
|
+
"romanian": "ro",
|
|
302
|
+
"ru": "ru",
|
|
303
|
+
"russian": "ru",
|
|
304
|
+
"sk": "sk",
|
|
305
|
+
"slovak": "sk",
|
|
306
|
+
"spanish": "es",
|
|
307
|
+
"sv": "sv",
|
|
308
|
+
"sv-se": "sv",
|
|
309
|
+
"swedish": "sv",
|
|
310
|
+
"th": "th",
|
|
311
|
+
"th-th": "th",
|
|
312
|
+
"thai": "th",
|
|
313
|
+
"tr": "tr",
|
|
314
|
+
"turkish": "tr",
|
|
315
|
+
"uk": "uk",
|
|
316
|
+
"ukrainian": "uk",
|
|
317
|
+
"vi": "vi",
|
|
318
|
+
"vietnamese": "vi",
|
|
319
|
+
"zh": "zh",
|
|
320
|
+
"zh-cn": "zh",
|
|
321
|
+
"zh-hans": "zh",
|
|
322
|
+
"zh-hant": "zh-TW",
|
|
323
|
+
"zh-hk": "zh-HK",
|
|
324
|
+
"zh-tw": "zh-TW",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def voice_providers(get: Callable[[str], str]) -> tuple[str, str]:
|
|
329
|
+
"""The (stt, tts) providers for the caller.
|
|
330
|
+
|
|
331
|
+
An explicit override wins; otherwise Cartesia when its key is present (richer, multi-language
|
|
332
|
+
voices), else Deepgram aura. `get` resolves a setting name for the calling lane, which reads
|
|
333
|
+
the environment locally and the run-scoped secrets when hosted.
|
|
334
|
+
"""
|
|
335
|
+
keyed = bool((get("CARTESIA_API_KEY") or "").strip())
|
|
336
|
+
default = "cartesia" if keyed else "deepgram"
|
|
337
|
+
stt = (get("SIMULATOR_STT_PROVIDER") or "").strip() or default
|
|
338
|
+
tts = (get("SIMULATOR_TTS_PROVIDER") or "").strip() or default
|
|
339
|
+
if tts == "deepgram" and not keyed and not (get("SIMULATOR_TTS_PROVIDER") or ""):
|
|
340
|
+
# Deepgram aura is one voice, so every persona sounds the same and the accent, language
|
|
341
|
+
# and gender the scenario chose are silently dropped. The call still runs, which is why
|
|
342
|
+
# this has to be said out loud rather than left to whoever listens to the recording.
|
|
343
|
+
logger.warning(
|
|
344
|
+
"cartesia_key_missing_personas_share_one_voice",
|
|
345
|
+
extra={"tts": "deepgram/aura-asteria-en"},
|
|
346
|
+
)
|
|
347
|
+
return stt, tts
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def transcriber_for(language: str) -> tuple[str, str, str]:
|
|
351
|
+
"""The (provider, model, language) a persona's language needs for speech to text.
|
|
352
|
+
|
|
353
|
+
Deepgram throughout, because Deepgram and Cartesia are the only providers configured. A
|
|
354
|
+
language Deepgram serves better multilingually is sent to that model instead of its own code.
|
|
355
|
+
"""
|
|
356
|
+
code = (language or "").lower()
|
|
357
|
+
if code.split("-", 1)[0] in _MULTILINGUAL_STT:
|
|
358
|
+
return ("deepgram", "nova-3", "multi")
|
|
359
|
+
return ("deepgram", "nova-3", language or "en-US")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def persona_stt_language(
|
|
363
|
+
persona: Mapping[str, object] | None, override: str = ""
|
|
364
|
+
) -> str:
|
|
365
|
+
"""The STT language for one caller, from the persona's languages.
|
|
366
|
+
|
|
367
|
+
An explicit override always wins. Otherwise the persona's first language is used, so a caller
|
|
368
|
+
who speaks Hindi is transcribed as Hindi rather than forced to English.
|
|
369
|
+
"""
|
|
370
|
+
if override and override.strip():
|
|
371
|
+
return override.strip()
|
|
372
|
+
languages = (persona or {}).get("languages") or []
|
|
373
|
+
if isinstance(languages, list) and languages:
|
|
374
|
+
first = str(languages[0]).strip().lower()
|
|
375
|
+
if first in _LANGUAGE_CODES:
|
|
376
|
+
return _LANGUAGE_CODES[first]
|
|
377
|
+
if 2 <= len(first) <= 5 and first.replace("-", "").isalpha():
|
|
378
|
+
return first
|
|
379
|
+
return "en"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
_CARTESIA_SUPPORTED_LANGS = frozenset(
|
|
383
|
+
{
|
|
384
|
+
"en",
|
|
385
|
+
"es",
|
|
386
|
+
"hi",
|
|
387
|
+
"de",
|
|
388
|
+
"fr",
|
|
389
|
+
"it",
|
|
390
|
+
"pl",
|
|
391
|
+
"ru",
|
|
392
|
+
"pt",
|
|
393
|
+
"ja",
|
|
394
|
+
"ko",
|
|
395
|
+
"zh",
|
|
396
|
+
"tr",
|
|
397
|
+
"sv",
|
|
398
|
+
"nl",
|
|
399
|
+
"no",
|
|
400
|
+
"te",
|
|
401
|
+
"kn",
|
|
402
|
+
"fi",
|
|
403
|
+
"mr",
|
|
404
|
+
"da",
|
|
405
|
+
"bn",
|
|
406
|
+
"sk",
|
|
407
|
+
"uk",
|
|
408
|
+
"el",
|
|
409
|
+
"ta",
|
|
410
|
+
"vi",
|
|
411
|
+
"id",
|
|
412
|
+
"ro",
|
|
413
|
+
"ka",
|
|
414
|
+
"ml",
|
|
415
|
+
"ms",
|
|
416
|
+
"he",
|
|
417
|
+
"bg",
|
|
418
|
+
"th",
|
|
419
|
+
"hu",
|
|
420
|
+
"pa",
|
|
421
|
+
"cs",
|
|
422
|
+
"tl",
|
|
423
|
+
"ar",
|
|
424
|
+
"gu",
|
|
425
|
+
"hr",
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
_CARTESIA_ACCENT_TO_LANG: dict[str, str] = {
|
|
429
|
+
"spanish": "es",
|
|
430
|
+
"south american": "es",
|
|
431
|
+
"indian": "hi",
|
|
432
|
+
"german": "de",
|
|
433
|
+
"french": "fr",
|
|
434
|
+
"italian": "it",
|
|
435
|
+
"polish": "pl",
|
|
436
|
+
"russian": "ru",
|
|
437
|
+
"portuguese": "pt",
|
|
438
|
+
"brazilian": "pt",
|
|
439
|
+
"japanese": "ja",
|
|
440
|
+
"korean": "ko",
|
|
441
|
+
"chinese": "zh",
|
|
442
|
+
"mandarin": "zh",
|
|
443
|
+
"turkish": "tr",
|
|
444
|
+
"swedish": "sv",
|
|
445
|
+
"dutch": "nl",
|
|
446
|
+
"norwegian": "no",
|
|
447
|
+
"finnish": "fi",
|
|
448
|
+
"danish": "da",
|
|
449
|
+
"slovak": "sk",
|
|
450
|
+
"ukrainian": "uk",
|
|
451
|
+
"greek": "el",
|
|
452
|
+
"romanian": "ro",
|
|
453
|
+
"georgian": "ka",
|
|
454
|
+
"bulgarian": "bg",
|
|
455
|
+
"thai": "th",
|
|
456
|
+
"hungarian": "hu",
|
|
457
|
+
"czech": "cs",
|
|
458
|
+
"croatian": "hr",
|
|
459
|
+
"vietnamese": "vi",
|
|
460
|
+
"indonesian": "id",
|
|
461
|
+
"malay": "ms",
|
|
462
|
+
"malaysian": "ms",
|
|
463
|
+
"tagalog": "tl",
|
|
464
|
+
"filipino": "tl",
|
|
465
|
+
"arabic": "ar",
|
|
466
|
+
"hebrew": "he",
|
|
467
|
+
"israeli": "he",
|
|
468
|
+
"telugu": "te",
|
|
469
|
+
"kannada": "kn",
|
|
470
|
+
"marathi": "mr",
|
|
471
|
+
"bengali": "bn",
|
|
472
|
+
"tamil": "ta",
|
|
473
|
+
"malayalam": "ml",
|
|
474
|
+
"punjabi": "pa",
|
|
475
|
+
"gujarati": "gu",
|
|
476
|
+
}
|
|
477
|
+
_CARTESIA_LANGUAGE_TO_LANG: dict[str, str] = {
|
|
478
|
+
"english": "en",
|
|
479
|
+
"chinese simplified": "zh",
|
|
480
|
+
"chinese traditional": "zh",
|
|
481
|
+
"hinglish": "hi",
|
|
482
|
+
"spanish": "es",
|
|
483
|
+
"hindi": "hi",
|
|
484
|
+
"german": "de",
|
|
485
|
+
"french": "fr",
|
|
486
|
+
"italian": "it",
|
|
487
|
+
"polish": "pl",
|
|
488
|
+
"russian": "ru",
|
|
489
|
+
"portuguese": "pt",
|
|
490
|
+
"japanese": "ja",
|
|
491
|
+
"korean": "ko",
|
|
492
|
+
"chinese": "zh",
|
|
493
|
+
"mandarin": "zh",
|
|
494
|
+
"turkish": "tr",
|
|
495
|
+
"swedish": "sv",
|
|
496
|
+
"dutch": "nl",
|
|
497
|
+
"norwegian": "no",
|
|
498
|
+
"telugu": "te",
|
|
499
|
+
"kannada": "kn",
|
|
500
|
+
"finnish": "fi",
|
|
501
|
+
"marathi": "mr",
|
|
502
|
+
"danish": "da",
|
|
503
|
+
"bengali": "bn",
|
|
504
|
+
"slovak": "sk",
|
|
505
|
+
"ukrainian": "uk",
|
|
506
|
+
"greek": "el",
|
|
507
|
+
"tamil": "ta",
|
|
508
|
+
"vietnamese": "vi",
|
|
509
|
+
"indonesian": "id",
|
|
510
|
+
"romanian": "ro",
|
|
511
|
+
"georgian": "ka",
|
|
512
|
+
"malayalam": "ml",
|
|
513
|
+
"malay": "ms",
|
|
514
|
+
"hebrew": "he",
|
|
515
|
+
"bulgarian": "bg",
|
|
516
|
+
"thai": "th",
|
|
517
|
+
"hungarian": "hu",
|
|
518
|
+
"punjabi": "pa",
|
|
519
|
+
"czech": "cs",
|
|
520
|
+
"tagalog": "tl",
|
|
521
|
+
"filipino": "tl",
|
|
522
|
+
"arabic": "ar",
|
|
523
|
+
"gujarati": "gu",
|
|
524
|
+
"croatian": "hr",
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _norm(value) -> str:
|
|
529
|
+
return str(value or "").strip().lower().replace("-", " ")
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@lru_cache(maxsize=1)
|
|
533
|
+
def _cartesia_catalog() -> dict:
|
|
534
|
+
path = Path(__file__).parent / "run" / "data" / "voices_by_language_and_gender.json"
|
|
535
|
+
try:
|
|
536
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
537
|
+
except (OSError, ValueError):
|
|
538
|
+
return {}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _persona_language_name(persona: dict) -> str:
|
|
542
|
+
languages = persona.get("languages")
|
|
543
|
+
if isinstance(languages, list) and languages:
|
|
544
|
+
return _norm(languages[0])
|
|
545
|
+
return _norm(persona.get("language"))
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _cartesia_lang_key(persona: dict) -> str:
|
|
549
|
+
"""The catalog language bucket for a persona: accent wins, then language, else English."""
|
|
550
|
+
accent = _norm(persona.get("accent"))
|
|
551
|
+
key = _CARTESIA_ACCENT_TO_LANG.get(accent)
|
|
552
|
+
if key in _CARTESIA_SUPPORTED_LANGS:
|
|
553
|
+
return key
|
|
554
|
+
language = _persona_language_name(persona)
|
|
555
|
+
key = _CARTESIA_LANGUAGE_TO_LANG.get(language)
|
|
556
|
+
if key in _CARTESIA_SUPPORTED_LANGS:
|
|
557
|
+
return key
|
|
558
|
+
if language in _CARTESIA_SUPPORTED_LANGS:
|
|
559
|
+
return language
|
|
560
|
+
return "en"
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def cartesia_voice_for(persona: dict) -> str:
|
|
564
|
+
"""A stable Cartesia voice id for one caller, chosen by accent/language and gender.
|
|
565
|
+
|
|
566
|
+
Deterministic by persona name so a caller keeps its voice across runs while a suite still
|
|
567
|
+
spreads voices. Falls back across gender and to English when a long-tail language lacks one.
|
|
568
|
+
"""
|
|
569
|
+
gender = _norm(persona.get("gender"))
|
|
570
|
+
if gender not in ("male", "female"):
|
|
571
|
+
gender = "female"
|
|
572
|
+
catalog = _cartesia_catalog()
|
|
573
|
+
key = _cartesia_lang_key(persona)
|
|
574
|
+
other = "male" if gender == "female" else "female"
|
|
575
|
+
voices = (
|
|
576
|
+
(catalog.get(key) or {}).get(gender)
|
|
577
|
+
or (catalog.get(key) or {}).get(other)
|
|
578
|
+
or (catalog.get("en") or {}).get(gender)
|
|
579
|
+
or []
|
|
580
|
+
)
|
|
581
|
+
if not voices:
|
|
582
|
+
return CARTESIA_DEFAULT_VOICE
|
|
583
|
+
index = sum(ord(character) for character in str(persona.get("name") or "")) % len(
|
|
584
|
+
voices
|
|
585
|
+
)
|
|
586
|
+
return voices[index]
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# How fast this person talks. Derived from the persona rather than randomised, so a rerun of the
|
|
590
|
+
# same scenario sounds the same -- a rate that moves between runs makes two recordings of one
|
|
591
|
+
# scenario incomparable. Cartesia documents 0.6 to 2.0 for sonic-3; this stays close to natural
|
|
592
|
+
# because the point is that callers differ from each other, not that any of them sounds odd.
|
|
593
|
+
_SPEECH_RATES = (0.9, 0.95, 1.0, 1.05, 1.12)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def persona_speech_rate(persona: Mapping[str, Any] | None) -> float:
|
|
597
|
+
"""A stable speech rate for this person.
|
|
598
|
+
|
|
599
|
+
Keyed on the same field as the voice, so the two move together: a persona keeps one voice and
|
|
600
|
+
one pace for as long as its name is the same.
|
|
601
|
+
"""
|
|
602
|
+
if not isinstance(persona, Mapping):
|
|
603
|
+
return 1.0
|
|
604
|
+
name = str(persona.get("name") or "").strip()
|
|
605
|
+
if not name:
|
|
606
|
+
return 1.0
|
|
607
|
+
return _SPEECH_RATES[sum(ord(character) for character in name) % len(_SPEECH_RATES)]
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# The only emotion names and levels sonic-3 accepts, established against the live API. It rejects
|
|
611
|
+
# name and level separately with HTTP 400, so nothing outside this set is ever sent.
|
|
612
|
+
_CARTESIA_EMOTION_NAMES = frozenset({"anger", "positivity", "surprise", "sadness", "curiosity"})
|
|
613
|
+
_CARTESIA_EMOTION_LEVELS = frozenset({"lowest", "low", "high", "highest"})
|
|
614
|
+
|
|
615
|
+
# What a personality sounds like, as a baseline colour for the whole call. A caller's feeling really
|
|
616
|
+
# moves during a call and this control does not, so it is a starting register rather than an arc:
|
|
617
|
+
# two personas that read the same on paper stop sounding identical. Anything unrecognised gets no
|
|
618
|
+
# control at all, which is the provider default and the behaviour before this existed.
|
|
619
|
+
_PERSONALITY_EMOTION = (
|
|
620
|
+
(("warm", "friendly", "cheerful", "enthusiastic", "chatty", "upbeat"), "positivity:high"),
|
|
621
|
+
(("professional", "formal", "businesslike", "direct", "efficient"), "positivity:low"),
|
|
622
|
+
(("irritated", "annoyed", "frustrated", "angry", "impatient", "abrupt"), "anger:low"),
|
|
623
|
+
(("curious", "inquisitive", "questioning", "sceptical", "skeptical"), "curiosity:high"),
|
|
624
|
+
(("anxious", "worried", "nervous", "distressed", "upset", "sad"), "sadness:low"),
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def persona_emotion(persona: Mapping[str, Any] | None) -> list[str]:
|
|
629
|
+
"""The baseline emotional colour for this person, or nothing where none is recognised."""
|
|
630
|
+
if not isinstance(persona, Mapping):
|
|
631
|
+
return []
|
|
632
|
+
described = " ".join(
|
|
633
|
+
str(persona.get(key) or "") for key in ("personality", "communication_style", "traits")
|
|
634
|
+
).lower()
|
|
635
|
+
for words, emotion in _PERSONALITY_EMOTION:
|
|
636
|
+
if any(word in described for word in words):
|
|
637
|
+
name, _, level = emotion.partition(":")
|
|
638
|
+
if name in _CARTESIA_EMOTION_NAMES and level in _CARTESIA_EMOTION_LEVELS:
|
|
639
|
+
return [emotion]
|
|
640
|
+
return []
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
_AURA_BY_ACCENT: dict[str, dict[str, list[str]]] = {
|
|
644
|
+
"american": {
|
|
645
|
+
"female": ["aura-asteria-en", "aura-luna-en", "aura-hera-en", "aura-stella-en"],
|
|
646
|
+
"male": ["aura-orion-en", "aura-arcas-en", "aura-perseus-en", "aura-zeus-en"],
|
|
647
|
+
},
|
|
648
|
+
"british": {"female": ["aura-athena-en"], "male": ["aura-helios-en"]},
|
|
649
|
+
"irish": {"female": ["aura-athena-en"], "male": ["aura-angus-en"]},
|
|
650
|
+
"australian": {"female": ["aura-athena-en"], "male": ["aura-helios-en"]},
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def aura_voice_for(persona: dict) -> str:
|
|
655
|
+
"""A stable aura voice for one caller, chosen by accent and gender.
|
|
656
|
+
|
|
657
|
+
Callers who share an accent still differ: the voice within the accent's set is picked by the
|
|
658
|
+
persona name, so a suite varies without being random between runs of the same scenario.
|
|
659
|
+
"""
|
|
660
|
+
accent = str(persona.get("accent") or "").strip().lower()
|
|
661
|
+
gender = str(persona.get("gender") or "").strip().lower()
|
|
662
|
+
if gender not in ("male", "female"):
|
|
663
|
+
gender = "female"
|
|
664
|
+
bucket = next(
|
|
665
|
+
(voices for key, voices in _AURA_BY_ACCENT.items() if key in accent),
|
|
666
|
+
_AURA_BY_ACCENT["american"],
|
|
667
|
+
)
|
|
668
|
+
voices = bucket.get(gender) or next(iter(bucket.values()))
|
|
669
|
+
index = sum(ord(character) for character in str(persona.get("name") or "")) % len(
|
|
670
|
+
voices
|
|
671
|
+
)
|
|
672
|
+
return voices[index]
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def simulator_definition(
|
|
676
|
+
get: Callable[[str], str], persona: Mapping[str, Any] | None = None
|
|
677
|
+
) -> "simulate.SimulatorAgentDefinition":
|
|
678
|
+
"""The caller's brain and voice. `get` resolves one setting for the calling lane.
|
|
679
|
+
|
|
680
|
+
Speech follows the persona's language: a caller who speaks Japanese cannot be transcribed
|
|
681
|
+
as English.
|
|
682
|
+
"""
|
|
683
|
+
llm_provider = (get("SIMULATOR_LLM_PROVIDER") or "").strip() or "google"
|
|
684
|
+
stt_override = (get("SIMULATOR_STT_PROVIDER") or "").strip()
|
|
685
|
+
_, tts_provider = voice_providers(get)
|
|
686
|
+
language = persona_stt_language(
|
|
687
|
+
dict(persona or {}), (get("SIMULATOR_STT_LANGUAGE") or "").strip()
|
|
688
|
+
)
|
|
689
|
+
stt_default_provider, stt_model, stt_language = transcriber_for(language)
|
|
690
|
+
stt_provider = stt_override or stt_default_provider
|
|
691
|
+
defaults = {
|
|
692
|
+
"llm": {"google": "gemini-2.5-flash", "openai": "gpt-4o-mini"},
|
|
693
|
+
"stt": {"deepgram": stt_model, "cartesia": "ink-2", "google": "chirp_2"},
|
|
694
|
+
"tts": {
|
|
695
|
+
"deepgram": "aura-asteria-en",
|
|
696
|
+
"cartesia": "sonic-3.5",
|
|
697
|
+
"google": "en-US-Chirp3-HD-Aoede",
|
|
698
|
+
},
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
def model(kind: str, provider: str) -> str:
|
|
702
|
+
return (get(f"SIMULATOR_{kind.upper()}_MODEL") or "").strip() or defaults[
|
|
703
|
+
kind
|
|
704
|
+
].get(provider.lower(), next(iter(defaults[kind].values())))
|
|
705
|
+
|
|
706
|
+
# Voice and model are different fields: aura encodes the speaker in the model name, Cartesia
|
|
707
|
+
# takes a voice id. Sending the model as the voice silently breaks Cartesia.
|
|
708
|
+
default_voice = (
|
|
709
|
+
CARTESIA_DEFAULT_VOICE
|
|
710
|
+
if tts_provider.lower() == "cartesia"
|
|
711
|
+
else "aura-asteria-en"
|
|
712
|
+
)
|
|
713
|
+
return simulate.SimulatorAgentDefinition(
|
|
714
|
+
llm={
|
|
715
|
+
"provider": llm_provider,
|
|
716
|
+
"model": model("llm", llm_provider),
|
|
717
|
+
"temperature": float(
|
|
718
|
+
(get("SIMULATOR_LLM_TEMPERATURE") or "").strip() or "0.35"
|
|
719
|
+
),
|
|
720
|
+
},
|
|
721
|
+
stt={
|
|
722
|
+
"provider": stt_provider,
|
|
723
|
+
"model": model("stt", stt_provider),
|
|
724
|
+
"language": stt_language,
|
|
725
|
+
},
|
|
726
|
+
tts={
|
|
727
|
+
"provider": tts_provider,
|
|
728
|
+
"model": model("tts", tts_provider),
|
|
729
|
+
"voice": (get("SIMULATOR_TTS_VOICE") or "").strip() or default_voice,
|
|
730
|
+
"speed": persona_speech_rate(persona),
|
|
731
|
+
"emotion": persona_emotion(persona),
|
|
732
|
+
},
|
|
733
|
+
instructions=simulator_instructions(
|
|
734
|
+
get("HARNESS_CALL_DIRECTION") or "",
|
|
735
|
+
get("HARNESS_CALLER_AWARENESS") or "",
|
|
736
|
+
get("HARNESS_ANSWERED_BY") or "",
|
|
737
|
+
get("HARNESS_VOICEMAIL_STYLE") or "",
|
|
738
|
+
recorded=bool((get("HARNESS_VOICEMAIL_CLIP") or "").strip()),
|
|
739
|
+
),
|
|
740
|
+
allow_interruptions=True,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
_CALLER_PHONE_KEYS = ("caller_phone", "caller_ani", "ani")
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def fixture_caller_phone(fixture: Mapping[str, Any] | None) -> str:
|
|
748
|
+
"""The number the target must see for this scenario.
|
|
749
|
+
|
|
750
|
+
A key that names the caller wins wherever it sits, so a support line or a driver listed
|
|
751
|
+
alongside cannot take the call's identity. Only when no such key exists anywhere does a plain
|
|
752
|
+
``phone`` count, since a fixture that carries exactly one number means that one.
|
|
753
|
+
"""
|
|
754
|
+
if not isinstance(fixture, Mapping):
|
|
755
|
+
return ""
|
|
756
|
+
|
|
757
|
+
def scoped(value: Any) -> str:
|
|
758
|
+
if isinstance(value, Mapping):
|
|
759
|
+
for name in _CALLER_PHONE_KEYS:
|
|
760
|
+
candidate = str(value.get(name) or "").strip()
|
|
761
|
+
if candidate:
|
|
762
|
+
return candidate
|
|
763
|
+
for nested in value.values():
|
|
764
|
+
candidate = scoped(nested)
|
|
765
|
+
if candidate:
|
|
766
|
+
return candidate
|
|
767
|
+
return ""
|
|
768
|
+
|
|
769
|
+
def plain(value: Any) -> str:
|
|
770
|
+
if isinstance(value, Mapping):
|
|
771
|
+
candidate = str(value.get("phone") or "").strip()
|
|
772
|
+
if candidate:
|
|
773
|
+
return candidate
|
|
774
|
+
for nested in value.values():
|
|
775
|
+
candidate = plain(nested)
|
|
776
|
+
if candidate:
|
|
777
|
+
return candidate
|
|
778
|
+
return ""
|
|
779
|
+
|
|
780
|
+
return scoped(fixture) or plain(fixture)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def caller_scenario(
|
|
784
|
+
*,
|
|
785
|
+
name: str,
|
|
786
|
+
persona: Mapping[str, Any] | None,
|
|
787
|
+
situation: str,
|
|
788
|
+
fixture: Mapping[str, Any] | None,
|
|
789
|
+
tts_provider: str,
|
|
790
|
+
outcome: str = "",
|
|
791
|
+
initial_message: str = "",
|
|
792
|
+
) -> "simulate.Scenario":
|
|
793
|
+
"""One simulated caller.
|
|
794
|
+
|
|
795
|
+
`outcome` is empty by default: the situation already says what this person wants in their own
|
|
796
|
+
words, and the grading criteria as an objective make the caller recite a checklist.
|
|
797
|
+
"""
|
|
798
|
+
persona = dict(persona) if isinstance(persona, Mapping) else {"name": "customer"}
|
|
799
|
+
persona["role"] = "customer"
|
|
800
|
+
provider = (tts_provider or "").lower()
|
|
801
|
+
# A voice from the persona's accent/language, so callers in one suite sound different.
|
|
802
|
+
if not persona.get("voice") and not persona.get("voice_id"):
|
|
803
|
+
if provider == "cartesia":
|
|
804
|
+
persona["voice"] = cartesia_voice_for(persona)
|
|
805
|
+
elif provider == "deepgram":
|
|
806
|
+
persona["voice"] = aura_voice_for(persona)
|
|
807
|
+
fixture = fixture if isinstance(fixture, Mapping) else {}
|
|
808
|
+
metadata = dict(persona.get("metadata") or {})
|
|
809
|
+
if caller_phone := fixture_caller_phone(fixture):
|
|
810
|
+
# LiveKit exposes this as participant metadata/attributes, so a target hydrates the
|
|
811
|
+
# seeded caller without knowing scenario internals.
|
|
812
|
+
metadata["caller_phone"] = caller_phone
|
|
813
|
+
persona["metadata"] = metadata
|
|
814
|
+
if initial_message.strip():
|
|
815
|
+
persona["initial_message"] = initial_message.strip()
|
|
816
|
+
knowledge = [
|
|
817
|
+
{
|
|
818
|
+
"key": str(key),
|
|
819
|
+
"value": json.dumps(value, ensure_ascii=False, default=str),
|
|
820
|
+
"disclosure": "on_request",
|
|
821
|
+
}
|
|
822
|
+
for key, value in fixture.items()
|
|
823
|
+
if key != "origin"
|
|
824
|
+
]
|
|
825
|
+
return simulate.Scenario(
|
|
826
|
+
name=name or "harness-voice",
|
|
827
|
+
dataset=[
|
|
828
|
+
simulate.Persona(
|
|
829
|
+
persona=persona,
|
|
830
|
+
situation=situation,
|
|
831
|
+
outcome=outcome,
|
|
832
|
+
knowledge=knowledge,
|
|
833
|
+
behavior_policy=dict(_BEHAVIOR_POLICY),
|
|
834
|
+
)
|
|
835
|
+
],
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def simulation_spec(
|
|
840
|
+
*,
|
|
841
|
+
run_id: str,
|
|
842
|
+
room_name: str,
|
|
843
|
+
agent_name: str | None,
|
|
844
|
+
system_prompt: str,
|
|
845
|
+
livekit_url: str,
|
|
846
|
+
recording_dir: Path,
|
|
847
|
+
scenario: "simulate.Scenario",
|
|
848
|
+
simulator: "simulate.SimulatorAgentDefinition",
|
|
849
|
+
direction: str,
|
|
850
|
+
max_seconds: float,
|
|
851
|
+
min_turn_messages: int,
|
|
852
|
+
agent_first_silence_seconds: float,
|
|
853
|
+
run_seconds: float,
|
|
854
|
+
agent_definition: "simulate.AgentDefinition | None" = None,
|
|
855
|
+
) -> SimulationSpec:
|
|
856
|
+
"""The voice run both lanes execute. Only the target definition differs.
|
|
857
|
+
|
|
858
|
+
LiveKit workers use ``agent_name``. Provider-hosted targets (Vapi/Retell) pass an explicit
|
|
859
|
+
definition while retaining the exact same managed LiveKit caller runtime, timeout policy,
|
|
860
|
+
recording behavior, and simulated customer as the local/LiveKit lanes.
|
|
861
|
+
"""
|
|
862
|
+
params = {
|
|
863
|
+
"record_audio": True,
|
|
864
|
+
"recording_root": str(recording_dir),
|
|
865
|
+
"recording_case_directory": str(recording_dir),
|
|
866
|
+
"min_turn_messages": min_turn_messages,
|
|
867
|
+
"max_seconds": max_seconds,
|
|
868
|
+
"connect_timeout": CONNECT_TIMEOUT_SECONDS,
|
|
869
|
+
"readiness_timeout": READINESS_TIMEOUT_SECONDS,
|
|
870
|
+
"cleanup_timeout": CLEANUP_TIMEOUT_SECONDS,
|
|
871
|
+
"conversation_direction": direction,
|
|
872
|
+
"agent_first_silence_timeout_seconds": agent_first_silence_seconds,
|
|
873
|
+
}
|
|
874
|
+
agent = agent_definition
|
|
875
|
+
if agent is None:
|
|
876
|
+
if not agent_name:
|
|
877
|
+
raise ValueError("livekit_agent_name_unavailable")
|
|
878
|
+
agent = simulate.AgentDefinition(
|
|
879
|
+
name=_TARGET_NAME,
|
|
880
|
+
agent_name=agent_name,
|
|
881
|
+
system_prompt=system_prompt,
|
|
882
|
+
transport={"kind": "webrtc"},
|
|
883
|
+
)
|
|
884
|
+
runtime = simulate.LiveKitSimulatorRuntime(
|
|
885
|
+
url=livekit_url, room_name=room_name, room_mode="managed"
|
|
886
|
+
)
|
|
887
|
+
return SimulationSpec(
|
|
888
|
+
run_id=run_id,
|
|
889
|
+
environment=EnvironmentSpec(
|
|
890
|
+
adapter="voice",
|
|
891
|
+
world_kind="voice_telephony",
|
|
892
|
+
config={
|
|
893
|
+
"agent_definition": agent.model_dump(mode="json", exclude_none=True),
|
|
894
|
+
"livekit_runtime": runtime.model_dump(mode="json", exclude_none=True),
|
|
895
|
+
"simulator": simulator.model_dump(mode="json", exclude_none=True),
|
|
896
|
+
"params": params,
|
|
897
|
+
},
|
|
898
|
+
),
|
|
899
|
+
target=AgentEndpointSpec(adapter="webrtc"),
|
|
900
|
+
simulator=SimulatorPolicySpec(adapter="livekit_simulator"),
|
|
901
|
+
scenario=scenario,
|
|
902
|
+
# Keep the execution policy and the engine parameters aligned: disagreement makes
|
|
903
|
+
# planners see the opposite call direction from the engine that actually runs.
|
|
904
|
+
execution=ExecutionPolicy(
|
|
905
|
+
direction=direction, timeout=TimeoutPolicy(run_seconds=run_seconds)
|
|
906
|
+
),
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
__all__ = [
|
|
911
|
+
"CARTESIA_DEFAULT_VOICE",
|
|
912
|
+
"persona_speech_rate",
|
|
913
|
+
"persona_emotion",
|
|
914
|
+
"CLEANUP_TIMEOUT_SECONDS",
|
|
915
|
+
"CONNECT_TIMEOUT_SECONDS",
|
|
916
|
+
"READINESS_TIMEOUT_SECONDS",
|
|
917
|
+
"SIMULATOR_INSTRUCTIONS",
|
|
918
|
+
"simulator_instructions",
|
|
919
|
+
"aura_voice_for",
|
|
920
|
+
"caller_scenario",
|
|
921
|
+
"fixture_caller_phone",
|
|
922
|
+
"cartesia_voice_for",
|
|
923
|
+
"persona_stt_language",
|
|
924
|
+
"simulation_spec",
|
|
925
|
+
"simulator_definition",
|
|
926
|
+
"transcriber_for",
|
|
927
|
+
"voice_providers",
|
|
928
|
+
]
|