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,215 @@
|
|
|
1
|
+
"""Standing an engine up in a container, which is the part no engine does differently.
|
|
2
|
+
|
|
3
|
+
Pulling an image, giving it a free port, waiting for it to actually answer, tearing it down
|
|
4
|
+
and not leaking it when a run is killed: none of that is about Postgres. It is the same work
|
|
5
|
+
for MySQL, ClickHouse, Mongo or anything else the harness is ever asked to run, so it is
|
|
6
|
+
written once here.
|
|
7
|
+
|
|
8
|
+
What an engine contributes is only what genuinely differs -- how to reach it, how to read what
|
|
9
|
+
it holds, and how to put that back. That is a small surface deliberately, because the cost of
|
|
10
|
+
teaching the harness a new engine is the thing that decides whether "whatever the agent uses"
|
|
11
|
+
is real or just an aspiration.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import secrets
|
|
18
|
+
import subprocess
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from . import Held, StoreError
|
|
22
|
+
|
|
23
|
+
# How long to wait for a fresh container to start answering. The first run on a machine pulls
|
|
24
|
+
# the image, which dominates; afterwards this is a second or two.
|
|
25
|
+
READY_TIMEOUT_SECONDS = 180.0
|
|
26
|
+
|
|
27
|
+
# Marks every container this module starts, so strays from a killed run can be found and
|
|
28
|
+
# removed without guessing at names.
|
|
29
|
+
LABEL = "alk.harness.store"
|
|
30
|
+
|
|
31
|
+
# The network to join, when the harness is itself in a container. Publishing a port to the
|
|
32
|
+
# host's loopback is enough when the harness runs on the host, but from inside a container
|
|
33
|
+
# 127.0.0.1 is its own loopback and the engine is not there. Sharing a network instead lets
|
|
34
|
+
# the engine be reached by container name, on the port it actually listens on.
|
|
35
|
+
NETWORK = "ALK_DOCKER_NETWORK"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def docker(*args: str, check: bool = True) -> str:
|
|
39
|
+
"""Run a docker command, and turn its failure into something worth reading."""
|
|
40
|
+
try:
|
|
41
|
+
done = subprocess.run( # nosec B603: list args, never shell=True
|
|
42
|
+
("docker", *args), capture_output=True, text=True, check=False
|
|
43
|
+
)
|
|
44
|
+
except FileNotFoundError as exc: # pragma: no cover - depends on the machine
|
|
45
|
+
raise StoreError(
|
|
46
|
+
"docker is not on PATH, so no store can be stood up. Install Docker, or start "
|
|
47
|
+
"Colima, and try again."
|
|
48
|
+
) from exc
|
|
49
|
+
if check and done.returncode != 0:
|
|
50
|
+
raise StoreError(
|
|
51
|
+
f"docker {' '.join(args)} failed ({done.returncode}): "
|
|
52
|
+
f"{(done.stderr or done.stdout).strip()}"
|
|
53
|
+
)
|
|
54
|
+
return done.stdout.strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ContainerStore(Held):
|
|
58
|
+
"""An engine the harness runs in a container for the agent to be pointed at.
|
|
59
|
+
|
|
60
|
+
Started once for a suite and reset between scenarios: standing an engine up costs seconds
|
|
61
|
+
and putting its data back costs milliseconds, so the container stays and only its contents
|
|
62
|
+
move.
|
|
63
|
+
|
|
64
|
+
Subclasses supply ``image``, ``container_port``, the environment the image needs, and how
|
|
65
|
+
to read and restore what it holds. Everything else is here.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
engine: str = ""
|
|
69
|
+
image: str = ""
|
|
70
|
+
container_port: int = 0
|
|
71
|
+
# Environment the image needs to come up with a known user, password and database. Values
|
|
72
|
+
# are formatted with ``user``, ``password`` and ``database``.
|
|
73
|
+
boot_env: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
version: str | None = None,
|
|
78
|
+
image: str | None = None,
|
|
79
|
+
database: str = "alk",
|
|
80
|
+
user: str = "alk",
|
|
81
|
+
password: str | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
default = type(self).image
|
|
84
|
+
if image:
|
|
85
|
+
self.image = image
|
|
86
|
+
elif version:
|
|
87
|
+
self.image = f"{default.split(':')[0]}:{version}"
|
|
88
|
+
else:
|
|
89
|
+
self.image = default
|
|
90
|
+
self.database = database
|
|
91
|
+
self.user = user
|
|
92
|
+
self.password = password or secrets.token_hex(16)
|
|
93
|
+
self.container = f"alk-store-{secrets.token_hex(6)}"
|
|
94
|
+
self.network = os.environ.get(NETWORK, "").strip()
|
|
95
|
+
self.host = "127.0.0.1"
|
|
96
|
+
self.port: int | None = None
|
|
97
|
+
self._started = False
|
|
98
|
+
# Every script `apply` has run, in order. Saved beside the rows so a restore into a
|
|
99
|
+
# fresh container can stand the schema up before putting the rows back.
|
|
100
|
+
self.applied: list[str] = []
|
|
101
|
+
|
|
102
|
+
# -- lifecycle -------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def start(self) -> None:
|
|
105
|
+
"""Stand the container up and block until it answers. Idempotent."""
|
|
106
|
+
if self._started:
|
|
107
|
+
return
|
|
108
|
+
environment: list[str] = []
|
|
109
|
+
for name, template in self.boot_env.items():
|
|
110
|
+
environment += [
|
|
111
|
+
"--env",
|
|
112
|
+
f"{name}={template.format(user=self.user, password=self.password, database=self.database)}",
|
|
113
|
+
]
|
|
114
|
+
docker(
|
|
115
|
+
"run",
|
|
116
|
+
"--detach",
|
|
117
|
+
"--name",
|
|
118
|
+
self.container,
|
|
119
|
+
"--label",
|
|
120
|
+
f"{LABEL}=1",
|
|
121
|
+
*environment,
|
|
122
|
+
*(("--network", self.network) if self.network else ()),
|
|
123
|
+
# Bound to loopback and given whatever port is free, so parallel runs on one
|
|
124
|
+
# machine never collide. Kept even on a shared network, where it is what lets
|
|
125
|
+
# someone on the host open a client against a running scenario.
|
|
126
|
+
"--publish",
|
|
127
|
+
f"127.0.0.1::{self.container_port}",
|
|
128
|
+
self.image,
|
|
129
|
+
)
|
|
130
|
+
self._started = True
|
|
131
|
+
if self.network:
|
|
132
|
+
self.host, self.port = self.container, self.container_port
|
|
133
|
+
else:
|
|
134
|
+
self.port = self._published_port()
|
|
135
|
+
self._await_ready()
|
|
136
|
+
|
|
137
|
+
def stop(self) -> None:
|
|
138
|
+
"""Remove the container. Safe when it never started, so teardown needs no guard."""
|
|
139
|
+
if not self._started:
|
|
140
|
+
return
|
|
141
|
+
docker("rm", "--force", "--volumes", self.container, check=False)
|
|
142
|
+
self._started = False
|
|
143
|
+
self.port = None
|
|
144
|
+
|
|
145
|
+
def _published_port(self) -> int:
|
|
146
|
+
mapping = docker("port", self.container, f"{self.container_port}/tcp")
|
|
147
|
+
if not mapping:
|
|
148
|
+
raise StoreError(
|
|
149
|
+
f"{self.container} published no port for {self.container_port}/tcp"
|
|
150
|
+
)
|
|
151
|
+
# "127.0.0.1:32768", or several lines when both stacks are bound.
|
|
152
|
+
return int(mapping.splitlines()[0].rsplit(":", 1)[1])
|
|
153
|
+
|
|
154
|
+
def _await_ready(self) -> None:
|
|
155
|
+
"""Poll until the engine answers, and say what went wrong if it never does.
|
|
156
|
+
|
|
157
|
+
A container that is running is not an engine that is ready: most database images start,
|
|
158
|
+
run their own initialisation, restart once, and only then listen. Connecting is the
|
|
159
|
+
only honest test, which is why this asks the subclass to really connect rather than
|
|
160
|
+
checking that the process exists.
|
|
161
|
+
"""
|
|
162
|
+
deadline = time.monotonic() + READY_TIMEOUT_SECONDS
|
|
163
|
+
last: Exception | None = None
|
|
164
|
+
while time.monotonic() < deadline:
|
|
165
|
+
try:
|
|
166
|
+
self.probe()
|
|
167
|
+
return
|
|
168
|
+
except Exception as exc: # noqa: BLE001 - any failure means not ready yet
|
|
169
|
+
last = exc
|
|
170
|
+
time.sleep(0.25)
|
|
171
|
+
logs = docker("logs", "--tail", "20", self.container, check=False)
|
|
172
|
+
container = self.container
|
|
173
|
+
# A container that never answers is not a container to leave running -- `start()` already
|
|
174
|
+
# set `_started`, so without this the caller's own teardown never runs (nothing ever calls
|
|
175
|
+
# `stop()` on a store whose `start()` raised) and the container leaks for good.
|
|
176
|
+
self.stop()
|
|
177
|
+
raise StoreError(
|
|
178
|
+
f"{container} did not answer within {READY_TIMEOUT_SECONDS:.0f}s: {last}\n"
|
|
179
|
+
f"last lines of its log:\n{logs}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def probe(self) -> None:
|
|
183
|
+
"""Really talk to the engine. Anything raised means "not ready yet"."""
|
|
184
|
+
raise NotImplementedError
|
|
185
|
+
|
|
186
|
+
# -- what the agent is pointed at ------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def dsn(self) -> str:
|
|
189
|
+
"""The connection string to hand the agent, in place of its own."""
|
|
190
|
+
raise NotImplementedError
|
|
191
|
+
|
|
192
|
+
def env(self, variable: str) -> dict[str, str]:
|
|
193
|
+
"""The DSN under the name this agent reads it from.
|
|
194
|
+
|
|
195
|
+
Redirecting an agent is usually one environment variable, and which one is a fact about
|
|
196
|
+
the agent rather than about us -- so it is named by the caller, never assumed here.
|
|
197
|
+
"""
|
|
198
|
+
return {variable: self.dsn()}
|
|
199
|
+
|
|
200
|
+
def address(self) -> tuple[str, int]:
|
|
201
|
+
if not self._started or self.port is None:
|
|
202
|
+
raise StoreError("the store has not been started, so it has no address yet")
|
|
203
|
+
return self.host, self.port
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def strays() -> list[str]:
|
|
207
|
+
"""Containers the harness started that are still running.
|
|
208
|
+
|
|
209
|
+
A killed run leaves its container behind, and the next one has no way to know it is not the
|
|
210
|
+
owner. Naming them is enough; removing them is the caller's decision.
|
|
211
|
+
"""
|
|
212
|
+
listed = docker(
|
|
213
|
+
"ps", "--filter", f"label={LABEL}=1", "--format", "{{.Names}}", check=False
|
|
214
|
+
)
|
|
215
|
+
return [name for name in listed.splitlines() if name.strip()]
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""The agent's own data, held where the agent holds it.
|
|
2
|
+
|
|
3
|
+
Plenty of real agents keep their state in memory, loaded from files their repository ships, and
|
|
4
|
+
they are not unusual. There is no engine to stand up for those, no port and no connection string.
|
|
5
|
+
Standing up a database for them and hoping the agent notices would be exactly the replication
|
|
6
|
+
this path exists to avoid.
|
|
7
|
+
|
|
8
|
+
So the store is the structure itself, and the agent's own loader is what fills it. The tools
|
|
9
|
+
under test then run against that structure the same way they run in production, because it *is*
|
|
10
|
+
the thing they run against: unmodified code, its real data, and a copy taken before each scenario
|
|
11
|
+
so the next one starts where the last one began.
|
|
12
|
+
|
|
13
|
+
With no loader given, this holds nothing at all and says so. That is the honest description of a
|
|
14
|
+
world whose records the agent's code keeps on itself rather than in anything a store can reach,
|
|
15
|
+
and it exists so such a world is not described as a database it does not have.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import copy
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
24
|
+
|
|
25
|
+
from . import Snapshot, StoreError
|
|
26
|
+
|
|
27
|
+
# Carried alongside a record whose group is keyed rather than listed, because the key is usually
|
|
28
|
+
# the id a check needs to name and rebuilding the group without it would throw it away.
|
|
29
|
+
ID = "_id"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InProcessStore:
|
|
33
|
+
"""The agent's own in-memory data, as a store.
|
|
34
|
+
|
|
35
|
+
``loader`` is the agent's function, imported from the agent's repository and called, never
|
|
36
|
+
reimplemented, so what is held is what the agent would hold on a cold start.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
engine = "in_process"
|
|
40
|
+
key = "in_process"
|
|
41
|
+
# Deliberately not state.json, which the snapshot uses for the agent's own state object. Two
|
|
42
|
+
# different things sharing one filename means whichever is written second wins, and the world
|
|
43
|
+
# comes back with its records on the wrong side of the seam: the store empty, everything in
|
|
44
|
+
# the agent's state, and the mutation gate then emptying a store that was never holding it.
|
|
45
|
+
FILE = "collections.json"
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
database: str | Path = "",
|
|
50
|
+
*,
|
|
51
|
+
loader: Callable[[], dict[str, Any]] | None = None,
|
|
52
|
+
module: str = "",
|
|
53
|
+
function: str = "load_data",
|
|
54
|
+
root: str | Path = "",
|
|
55
|
+
**_ignored: Any,
|
|
56
|
+
) -> None:
|
|
57
|
+
# Takes the same arguments as any other store and uses most of them only when there is a
|
|
58
|
+
# loader, so opening one is the same call whichever kind it turns out to be.
|
|
59
|
+
self.database = str(database or "")
|
|
60
|
+
self.loader = loader
|
|
61
|
+
self.module = module
|
|
62
|
+
self.function = function
|
|
63
|
+
self.root = str(root or "")
|
|
64
|
+
self.data: dict[str, Any] = {}
|
|
65
|
+
self._started = False
|
|
66
|
+
|
|
67
|
+
# -- lifecycle -------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
def start(self) -> None:
|
|
70
|
+
"""Load the agent's data by calling the agent's own loader, if there is one."""
|
|
71
|
+
if self._started or (self.loader is None and not self.module):
|
|
72
|
+
return
|
|
73
|
+
if self.loader is None:
|
|
74
|
+
self.loader = self._imported()
|
|
75
|
+
loaded = self.loader()
|
|
76
|
+
if not isinstance(loaded, dict):
|
|
77
|
+
raise StoreError(
|
|
78
|
+
f"{self.function} returned {type(loaded).__name__}, not a dict of named groups, "
|
|
79
|
+
"so there is nothing a check could read by name"
|
|
80
|
+
)
|
|
81
|
+
self.data = loaded
|
|
82
|
+
self._started = True
|
|
83
|
+
|
|
84
|
+
def _imported(self) -> Callable[[], dict[str, Any]]:
|
|
85
|
+
"""The agent's loader, imported from the agent's repository.
|
|
86
|
+
|
|
87
|
+
Deliberately an import of their code rather than a reimplementation of it. If it will not
|
|
88
|
+
import, that is worth stopping for: the alternative is inventing data and grading the
|
|
89
|
+
agent against a world it has never seen.
|
|
90
|
+
"""
|
|
91
|
+
import importlib
|
|
92
|
+
import sys
|
|
93
|
+
|
|
94
|
+
if self.root and self.root not in sys.path:
|
|
95
|
+
sys.path.insert(0, self.root)
|
|
96
|
+
try:
|
|
97
|
+
found = importlib.import_module(self.module)
|
|
98
|
+
except ImportError as exc:
|
|
99
|
+
raise StoreError(
|
|
100
|
+
f"cannot import {self.module!r} from {self.root or 'sys.path'}: {exc}. The "
|
|
101
|
+
"agent's own dependencies have to be importable for its loader to run."
|
|
102
|
+
) from exc
|
|
103
|
+
loader = getattr(found, self.function, None)
|
|
104
|
+
if not callable(loader):
|
|
105
|
+
raise StoreError(f"{self.module}.{self.function} is not a function")
|
|
106
|
+
return loader
|
|
107
|
+
|
|
108
|
+
def stop(self) -> None:
|
|
109
|
+
self.data = {}
|
|
110
|
+
self._started = False
|
|
111
|
+
|
|
112
|
+
def dsn(self) -> str:
|
|
113
|
+
"""Nothing connects to this, which is the point.
|
|
114
|
+
|
|
115
|
+
Reported rather than raised: a store with no address is a fact about this kind of agent,
|
|
116
|
+
not a failure, and it is recorded so nothing later goes looking for a connection string
|
|
117
|
+
that was never going to exist.
|
|
118
|
+
"""
|
|
119
|
+
return "inprocess://"
|
|
120
|
+
|
|
121
|
+
# -- statements ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def apply(self, script: str) -> None:
|
|
124
|
+
"""Run a snippet against the data, with ``data`` in scope and nothing else.
|
|
125
|
+
|
|
126
|
+
How a seed is expressed for a store with no query language: the same Python the agent's
|
|
127
|
+
own code would use to reach into its structures.
|
|
128
|
+
"""
|
|
129
|
+
if not script.strip():
|
|
130
|
+
return
|
|
131
|
+
namespace: dict[str, Any] = {"data": self.data, "json": json}
|
|
132
|
+
try:
|
|
133
|
+
exec(compile(script, "<seed>", "exec"), namespace) # nosec B102
|
|
134
|
+
except Exception as exc: # noqa: BLE001 - the caller's snippet, reported as given
|
|
135
|
+
raise StoreError(f"{type(exc).__name__}: {exc}") from exc
|
|
136
|
+
|
|
137
|
+
def execute(self, statement: str, params: Sequence[Any] = ()) -> int:
|
|
138
|
+
raise StoreError(
|
|
139
|
+
"this agent keeps its state in its own code, so there is no query language to run "
|
|
140
|
+
"statements in. Change the world through the agent's own tools, or through "
|
|
141
|
+
"world.put, world.change and world.drop."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def query(self, statement: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]:
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
# -- records ---------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def collections(self) -> list[str]:
|
|
150
|
+
return sorted(self.data)
|
|
151
|
+
|
|
152
|
+
def holds(self, collection: str) -> bool:
|
|
153
|
+
return collection in self.data
|
|
154
|
+
|
|
155
|
+
def records(self, collection: str) -> list[dict[str, Any]]:
|
|
156
|
+
return self._rows(self.data.get(collection))
|
|
157
|
+
|
|
158
|
+
def state(self) -> dict[str, list[dict[str, Any]]]:
|
|
159
|
+
"""Every group and its records, in the shape the checks already expect.
|
|
160
|
+
|
|
161
|
+
The agent's structures are usually keyed by id rather than listed, so a mapping becomes
|
|
162
|
+
records with the key carried along. Without that a check counting records in a group
|
|
163
|
+
would be counting nothing, and the id it needs to name would have been thrown away.
|
|
164
|
+
"""
|
|
165
|
+
return {name: self._rows(group) for name, group in self.data.items()}
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _rows(group: Any) -> list[dict[str, Any]]:
|
|
169
|
+
if isinstance(group, dict):
|
|
170
|
+
return [
|
|
171
|
+
{ID: key, **value}
|
|
172
|
+
if isinstance(value, dict)
|
|
173
|
+
else {ID: key, "value": value}
|
|
174
|
+
for key, value in group.items()
|
|
175
|
+
]
|
|
176
|
+
if isinstance(group, list):
|
|
177
|
+
return [row if isinstance(row, dict) else {"value": row} for row in group]
|
|
178
|
+
if group is None:
|
|
179
|
+
return []
|
|
180
|
+
return [{"value": group}]
|
|
181
|
+
|
|
182
|
+
def start_collection(self, collection: str, *, keyed: bool = False) -> None:
|
|
183
|
+
"""Make a collection that does not exist yet.
|
|
184
|
+
|
|
185
|
+
For an agent with no store of its own, every collection is one the harness invents, so
|
|
186
|
+
there is nothing to declare them in advance the way a schema does for a database.
|
|
187
|
+
"""
|
|
188
|
+
if collection not in self.data:
|
|
189
|
+
self.data[collection] = {} if keyed else []
|
|
190
|
+
|
|
191
|
+
def add(self, collection: str, record: Mapping[str, Any]) -> int:
|
|
192
|
+
group = self.data.get(collection)
|
|
193
|
+
if isinstance(group, list):
|
|
194
|
+
group.append(dict(record))
|
|
195
|
+
return 1
|
|
196
|
+
if isinstance(group, dict):
|
|
197
|
+
written = dict(record)
|
|
198
|
+
identifier = written.pop(ID, None)
|
|
199
|
+
if identifier is None:
|
|
200
|
+
raise KeyError(
|
|
201
|
+
f"{collection} is keyed, so a new record needs its key given as {ID!r}"
|
|
202
|
+
)
|
|
203
|
+
group[identifier] = written
|
|
204
|
+
return 1
|
|
205
|
+
raise KeyError(f"no group {collection!r} here to add to")
|
|
206
|
+
|
|
207
|
+
def amend(
|
|
208
|
+
self, collection: str, key: str, changes: Mapping[str, Any], *, by: str = ""
|
|
209
|
+
) -> int:
|
|
210
|
+
group = self.data.get(collection)
|
|
211
|
+
if isinstance(group, dict) and not by:
|
|
212
|
+
if key not in group:
|
|
213
|
+
return 0
|
|
214
|
+
group[key].update(dict(changes))
|
|
215
|
+
return 1
|
|
216
|
+
for row in self._writable(collection, group, key, by):
|
|
217
|
+
row.update(dict(changes))
|
|
218
|
+
return len(self._writable(collection, group, key, by))
|
|
219
|
+
|
|
220
|
+
def remove(self, collection: str, key: str = "", *, by: str = "") -> int:
|
|
221
|
+
group = self.data.get(collection)
|
|
222
|
+
if isinstance(group, dict):
|
|
223
|
+
if not key:
|
|
224
|
+
gone = len(group)
|
|
225
|
+
group.clear()
|
|
226
|
+
return gone
|
|
227
|
+
if by:
|
|
228
|
+
matched = [
|
|
229
|
+
name for name, row in group.items() if _reads(row, by) == key
|
|
230
|
+
]
|
|
231
|
+
else:
|
|
232
|
+
matched = [key] if key in group else []
|
|
233
|
+
for name in matched:
|
|
234
|
+
group.pop(name, None)
|
|
235
|
+
return len(matched)
|
|
236
|
+
if isinstance(group, list):
|
|
237
|
+
if not key:
|
|
238
|
+
gone = len(group)
|
|
239
|
+
group.clear()
|
|
240
|
+
return gone
|
|
241
|
+
if not by:
|
|
242
|
+
raise KeyError(
|
|
243
|
+
f"{collection} is a list, so removing one record needs the field it is keyed on"
|
|
244
|
+
)
|
|
245
|
+
kept = [row for row in group if _reads(row, by) != key]
|
|
246
|
+
gone = len(group) - len(kept)
|
|
247
|
+
group[:] = kept
|
|
248
|
+
return gone
|
|
249
|
+
raise KeyError(f"no group {collection!r} here to remove from")
|
|
250
|
+
|
|
251
|
+
def _writable(
|
|
252
|
+
self, collection: str, group: Any, key: str, by: str
|
|
253
|
+
) -> list[dict[str, Any]]:
|
|
254
|
+
if group is None:
|
|
255
|
+
raise KeyError(f"no group {collection!r} here to change")
|
|
256
|
+
if not by:
|
|
257
|
+
raise KeyError(
|
|
258
|
+
f"{collection} is a list, so changing a record needs the field it is keyed on"
|
|
259
|
+
)
|
|
260
|
+
if isinstance(group, dict):
|
|
261
|
+
# A keyed group stores the key as the mapping's key, because `add` pops ``_id`` out
|
|
262
|
+
# of the record to put it there. So asking to match on ``_id`` finds nothing, changes
|
|
263
|
+
# nothing, and returns zero, which a scenario's setup does not look at: the run is
|
|
264
|
+
# then graded against a world that was never set up. The key is answered here as
|
|
265
|
+
# though it were still a field, which is what whoever wrote it meant.
|
|
266
|
+
if by == ID and key in group:
|
|
267
|
+
row = group[key]
|
|
268
|
+
return [row] if isinstance(row, dict) else []
|
|
269
|
+
found: Any = group.values()
|
|
270
|
+
else:
|
|
271
|
+
found = group
|
|
272
|
+
return [
|
|
273
|
+
row for row in found if isinstance(row, dict) and _reads(row, by) == key
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
# -- going back ------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def clear(self) -> None:
|
|
279
|
+
"""Empty every group, keeping its shape: the agent's own code indexes into these."""
|
|
280
|
+
for name, group in self.data.items():
|
|
281
|
+
if isinstance(group, dict):
|
|
282
|
+
group.clear()
|
|
283
|
+
elif isinstance(group, list):
|
|
284
|
+
group.clear()
|
|
285
|
+
else:
|
|
286
|
+
self.data[name] = None
|
|
287
|
+
|
|
288
|
+
def freeze(self) -> Snapshot:
|
|
289
|
+
"""A deep copy. Nothing sits behind these records, so there are no counters to carry."""
|
|
290
|
+
return Snapshot(rows=copy.deepcopy(self.state()), counters={})
|
|
291
|
+
|
|
292
|
+
def restore(self, snapshot: Snapshot) -> None:
|
|
293
|
+
"""Put the structure back the way the agent's loader left it.
|
|
294
|
+
|
|
295
|
+
Rebuilt from the records rather than kept as a second copy, so restore is checked against
|
|
296
|
+
exactly what ``state`` reports: the thing the gate compares and the thing a check reads
|
|
297
|
+
are then the same thing, and cannot drift apart.
|
|
298
|
+
"""
|
|
299
|
+
rebuilt: dict[str, Any] = {}
|
|
300
|
+
for name, rows in snapshot.rows.items():
|
|
301
|
+
original = self.data.get(name)
|
|
302
|
+
if isinstance(original, list):
|
|
303
|
+
rebuilt[name] = [
|
|
304
|
+
row["value"] if set(row) == {"value"} else dict(row)
|
|
305
|
+
for row in copy.deepcopy(rows)
|
|
306
|
+
]
|
|
307
|
+
continue
|
|
308
|
+
keyed: dict[str, Any] = {}
|
|
309
|
+
for row in copy.deepcopy(rows):
|
|
310
|
+
identifier = row.pop(ID, None)
|
|
311
|
+
if identifier is None:
|
|
312
|
+
continue
|
|
313
|
+
keyed[identifier] = row.get("value") if set(row) == {"value"} else row
|
|
314
|
+
rebuilt[name] = keyed
|
|
315
|
+
# A group the snapshot does not mention is emptied, not carried over: restore has to be
|
|
316
|
+
# able to reproduce a snapshot that holds nothing, or the gate cannot empty the store to
|
|
317
|
+
# find out whether the checks actually bite. The key itself stays, with its original
|
|
318
|
+
# shape, because the agent's own code indexes into it and would not survive its absence.
|
|
319
|
+
for name, group in self.data.items():
|
|
320
|
+
if name not in rebuilt:
|
|
321
|
+
rebuilt[name] = [] if isinstance(group, list) else {}
|
|
322
|
+
self.data.clear()
|
|
323
|
+
self.data.update(rebuilt)
|
|
324
|
+
|
|
325
|
+
def save_to(self, path: str | Path) -> None:
|
|
326
|
+
if not self.data:
|
|
327
|
+
return
|
|
328
|
+
root = Path(path)
|
|
329
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
(root / self.FILE).write_text(
|
|
331
|
+
json.dumps(self.data, indent=2, default=str), encoding="utf-8"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def load_from(self, path: str | Path) -> None:
|
|
335
|
+
held = Path(path) / self.FILE
|
|
336
|
+
if not held.exists():
|
|
337
|
+
return
|
|
338
|
+
self.data = json.loads(held.read_text(encoding="utf-8"))
|
|
339
|
+
self._started = True
|
|
340
|
+
|
|
341
|
+
def close(self) -> None:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _reads(row: Any, field: str) -> Any:
|
|
346
|
+
return row.get(field) if isinstance(row, dict) else None
|