strata-agent-memory 0.2.7__tar.gz → 0.2.9__tar.gz
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.
- {strata_agent_memory-0.2.7/strata_agent_memory.egg-info → strata_agent_memory-0.2.9}/PKG-INFO +4 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/__init__.py +1 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/cli.py +47 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/compiler.py +128 -29
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/contracts.py +2 -1
- strata_agent_memory-0.2.9/agent_memory/endpoints.py +258 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/evidence.py +159 -16
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/kit.py +29 -6
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/lane.py +21 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/ledger.py +2 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/mcp_server.py +16 -7
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/remote.py +43 -5
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/render.py +7 -1
- strata_agent_memory-0.2.9/agent_memory/retrieval.py +878 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/serve.py +17 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/README.md +10 -2
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/agent-session.sh +30 -3
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/backup-tokens.sh +4 -2
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/install-hooks-mac.sh +25 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/install-replica-driver.sh +104 -71
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/onboard-agent.sh +9 -4
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/upgrade-driver.sh +23 -7
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/pyproject.toml +21 -4
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9/strata_agent_memory.egg-info}/PKG-INFO +4 -1
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/SOURCES.txt +17 -1
- strata_agent_memory-0.2.9/strata_agent_memory.egg-info/requires.txt +7 -0
- strata_agent_memory-0.2.9/tests/test_binding_refusal_97e36882.py +122 -0
- strata_agent_memory-0.2.9/tests/test_build_recheck_image_e4a93fd6.py +51 -0
- strata_agent_memory-0.2.9/tests/test_compact_window_778d310f.py +403 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_deploy_mac.py +25 -5
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_install_lane_cb430a30.py +1 -1
- strata_agent_memory-0.2.9/tests/test_memoryagentbench_60a8abfc.py +374 -0
- strata_agent_memory-0.2.9/tests/test_memsyco_7b16e8e4.py +553 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_packaging.py +2 -0
- strata_agent_memory-0.2.9/tests/test_proof_exclusions_e4a93fd6.py +80 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_recheck_sandbox_543e48a7.py +24 -2
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_client.py +1 -1
- strata_agent_memory-0.2.9/tests/test_reply_cap_cfbd7681.py +229 -0
- strata_agent_memory-0.2.9/tests/test_retrieval_llm_9e166c2f.py +907 -0
- strata_agent_memory-0.2.9/tests/test_retrieval_plugin_9e166c2f.py +364 -0
- strata_agent_memory-0.2.9/tests/test_retrieval_vectors_9e166c2f.py +648 -0
- strata_agent_memory-0.2.9/tests/test_sandbox_image_e4a93fd6.py +320 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_served_ledger_6e8e49f3.py +4 -0
- strata_agent_memory-0.2.9/tests/test_taskset_66bac366.py +356 -0
- strata_agent_memory-0.2.9/tests/test_tools_clone_4b72f0c1.py +232 -0
- strata_agent_memory-0.2.9/tests/test_verify_branch_aff56f06.py +255 -0
- strata_agent_memory-0.2.7/strata_agent_memory.egg-info/requires.txt +0 -3
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/LICENSE +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/README.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/__main__.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/approvals.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/audit.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/compact.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/export.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/hooks.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/identity.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/instances.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/measure.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/memorizer.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/pathmap.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/recheck.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/records.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/relay/__init__.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/relay/adapters.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/relay/cli.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/relay/client.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/relay/core.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/reliability.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/roster.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/signing.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/stats.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/supersession.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/trust.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/agent_memory/wake.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/lib/served-ledger.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/agent-keys.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/install-relay-driver.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/install-runner-driver.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/instances.example.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/relay-agent.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/seed-board.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/seed-mail-context.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/mac/seed-topics.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/.claude-plugin/marketplace.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/README.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/bin/agent-memory-approvals +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/bin/agent-memory-wake +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/hooks/hooks.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/monitors/monitors.json +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/rules/approvals.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/approvals/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/approve/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/inbox/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/reject/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-answer/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-answer/agents/openai.yaml +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/SKILL.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/agents/openai.yaml +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/rules/approvals.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/scripts/agent-memory-approvals +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/relay/README.md +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/relay/agent-memory-relay@.service +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/relay/install-relay-mac.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/relay/install-relay.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/relay/macos/com.strata.agent-memory.relay.plist.template +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/deploy/relay/relay-launch.sh +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/setup.cfg +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/entry_points.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/top_level.txt +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_assessment_67b26eae.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_audit.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_audit_lane_numbers_ea89ba26.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_audit_measurement_13190a20.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_backlog_check.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_changelog_fragments.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_changelog_guard_shallow_57a2f78e.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_ci_docs_only_353ce010.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_cli.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_compact.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_compiler.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_compiler_render.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_config.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_contract_amend_649e0b45.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_contracts.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_deploy_relay.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_deploy_replica.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_deploy_tokens.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_evidence.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_find_root_b4b5019b.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_hooks.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_identity.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_issue_bridge.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_lane_63b7cf8b.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_lane_derived_roots_817ab446.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_lane_nightly_84b22b63.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_lane_priority_779d51ba.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_lane_stop_63b7cf8b.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_ledger.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_ledger_index.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_live_wake_c217c336.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_local_lanes_sibling_1411a2b2.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_memorizer.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_outcome_commit_binding_b1de564d.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_outcome_needs_evidence_b1de564d.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_pathmap.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_phase2.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_plugins.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_receipt_reports_274e6199.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_recheck.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_recheck_away_207af145.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_recheck_remote_8185d2d3.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_recheck_sandbox_nits_543e48a7.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_records.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_redaction.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_adapters.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_codex.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_core.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_instance_2a2e3887.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_release_526f5261.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_relay_release_gate_b3b3e487.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_reliability.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_remote.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_remote_signing.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_review2_fixes.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_review3_fixes.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_review_fixes.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_rollback_78710ebf.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_scale.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_serve.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_signing.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_sit_fleet_1b24b0b1.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_sit_fleet_ci_0d9b76a2.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_sit_var_bindings_e802ca84.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_stats.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_supersedes_61416280.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_trust.py +0 -0
- {strata_agent_memory-0.2.7 → strata_agent_memory-0.2.9}/tests/test_uv_lock_env_274e6199.py +0 -0
{strata_agent_memory-0.2.7/strata_agent_memory.egg-info → strata_agent_memory-0.2.9}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strata-agent-memory
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Portable, auditable, cross-vendor memory for LLM agents: evidence-bound claims, trust tiers, and compiled context views.
|
|
5
5
|
Author: Strata Intelligence
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,6 +20,9 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Provides-Extra: sign
|
|
22
22
|
Requires-Dist: cryptography>=42; extra == "sign"
|
|
23
|
+
Provides-Extra: retrieval
|
|
24
|
+
Requires-Dist: sqlite-vec>=0.1.9; extra == "retrieval"
|
|
25
|
+
Requires-Dist: fastembed>=0.8; extra == "retrieval"
|
|
23
26
|
Dynamic: license-file
|
|
24
27
|
|
|
25
28
|
# agent-memory
|
|
@@ -987,6 +987,39 @@ def cmd_memorize(args) -> int:
|
|
|
987
987
|
return 0
|
|
988
988
|
|
|
989
989
|
|
|
990
|
+
def cmd_retrieval(args) -> int:
|
|
991
|
+
"""The retrieval plugin's two steps outside compile (ctr_9e166c2f291b829b): fetch-model,
|
|
992
|
+
the one that opens a socket, and index, which embeds the records (compile embeds only
|
|
993
|
+
the task, so its cost never grows with the ledger)."""
|
|
994
|
+
from . import retrieval as retrieval_mod
|
|
995
|
+
try:
|
|
996
|
+
if args.action == "fetch-model":
|
|
997
|
+
model = args.model
|
|
998
|
+
if model is None:
|
|
999
|
+
try:
|
|
1000
|
+
cfg = retrieval_mod.settings(Ledger.open().config)
|
|
1001
|
+
except FileNotFoundError:
|
|
1002
|
+
cfg = None # no ledger here: the default model
|
|
1003
|
+
embed = (cfg or {}).get("embed") or {}
|
|
1004
|
+
if embed.get("backend") == "endpoint":
|
|
1005
|
+
print(f"retrieval fetch-model: config.retrieval.embed names the endpoint {embed['endpoint']}, "
|
|
1006
|
+
f"which serves its model: there is nothing to fetch", file=sys.stderr)
|
|
1007
|
+
return 2
|
|
1008
|
+
model = embed.get("model", retrieval_mod.DEFAULT_MODEL)
|
|
1009
|
+
_print(retrieval_mod.fetch_model(model))
|
|
1010
|
+
return 0
|
|
1011
|
+
ledger = Ledger.open()
|
|
1012
|
+
cfg = retrieval_mod.settings(ledger.config)
|
|
1013
|
+
if cfg is None:
|
|
1014
|
+
print("config.retrieval names no plugin: there is no index to build", file=sys.stderr)
|
|
1015
|
+
return 2
|
|
1016
|
+
_print(retrieval_mod.index(ledger, cfg))
|
|
1017
|
+
return 0
|
|
1018
|
+
except retrieval_mod.RetrievalError as exc:
|
|
1019
|
+
print(f"retrieval {args.action}: {exc}", file=sys.stderr)
|
|
1020
|
+
return 1
|
|
1021
|
+
|
|
1022
|
+
|
|
990
1023
|
def cmd_export(args) -> int:
|
|
991
1024
|
ledger = Ledger.open()
|
|
992
1025
|
reader = _author(args, ledger) # another user's private receipts are not ours to export
|
|
@@ -1073,7 +1106,7 @@ def cmd_serve(args) -> int:
|
|
|
1073
1106
|
|
|
1074
1107
|
# the local ledger is the whole of what these do; there is nothing to send
|
|
1075
1108
|
LOCAL_ONLY = ("init", "trust", "untrust", "fsck", "redact", "keygen", "roster", "serve",
|
|
1076
|
-
"mcp", "compact", "memorize")
|
|
1109
|
+
"mcp", "compact", "memorize", "retrieval")
|
|
1077
1110
|
# answered over --remote, --instance or $AGENT_MEMORY_REMOTE, and never because a checkout's
|
|
1078
1111
|
# committed config names a replica: the replica's own clone carries that file and still
|
|
1079
1112
|
# re-checks locally, and so does the nightly CI lane in a checkout that names one
|
|
@@ -2527,6 +2560,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2527
2560
|
_add_identity(s)
|
|
2528
2561
|
s.set_defaults(fn=cmd_memorize)
|
|
2529
2562
|
|
|
2563
|
+
s = sub.add_parser("retrieval", help="the reader-side retrieval plugin's steps outside compile "
|
|
2564
|
+
"(config.retrieval, docs/PROTOCOL.md section 6)")
|
|
2565
|
+
rs = s.add_subparsers(dest="action", required=True)
|
|
2566
|
+
r = rs.add_parser("fetch-model", help="download the embedding model's weights into the cache: the one "
|
|
2567
|
+
"step that opens a socket; compile loads them offline")
|
|
2568
|
+
r.add_argument("--model", help="one of the models the plugin knows (default: config.retrieval.embed's "
|
|
2569
|
+
"model, else nomic-ai/nomic-embed-text-v1.5-Q)")
|
|
2570
|
+
rs.add_parser("index", help="bring this ledger's retrieval index up to the ledger and, with "
|
|
2571
|
+
"config.retrieval.restate, restate every record without a restatement, and "
|
|
2572
|
+
"with config.retrieval.embed, embed every record without a vector: compile "
|
|
2573
|
+
"does neither, so run this after records arrive")
|
|
2574
|
+
s.set_defaults(fn=cmd_retrieval)
|
|
2575
|
+
|
|
2530
2576
|
s = sub.add_parser("export", help="export the ledger in a standard format")
|
|
2531
2577
|
s.add_argument("--format", choices=list(export_mod.FORMATS), default="prov-json",
|
|
2532
2578
|
help="prov-json (W3C PROV for auditors), in-toto (one unsigned Statement per receipt), "
|
|
@@ -14,6 +14,7 @@ Roles and modes follow the fork/isolate rule:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
+
import sys
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from datetime import timedelta
|
|
19
20
|
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
|
@@ -142,6 +143,9 @@ class Compiled:
|
|
|
142
143
|
pinned_cap: Optional[int] = None
|
|
143
144
|
pinned_cut: Dict[str, int] = field(default_factory=dict)
|
|
144
145
|
pinned_full: Dict[str, int] = field(default_factory=dict)
|
|
146
|
+
# the retrieval plugin that took part (ctr_9e166c2f291b829b): {plugin, share, ids}, and
|
|
147
|
+
# error when it could not run; None when config.retrieval names none
|
|
148
|
+
retrieval: Optional[Dict] = None
|
|
145
149
|
|
|
146
150
|
def included_ids(self) -> List[str]:
|
|
147
151
|
return [i.id for items in self.sections.values() for i in items]
|
|
@@ -590,6 +594,10 @@ def compile_context(
|
|
|
590
594
|
else:
|
|
591
595
|
out.sections["working"].append(item)
|
|
592
596
|
|
|
597
|
+
# the retrieval plugin (ctr_9e166c2f291b829b): the ids it finds for the task, of which
|
|
598
|
+
# the reader sees only what this compile's own policy filter let through (candidates)
|
|
599
|
+
retrieval_cfg, retrieved = _retrieval(ledger, task, {item.id: item for item, _ in candidates}, out, assessment)
|
|
600
|
+
|
|
593
601
|
seen_dnr = set()
|
|
594
602
|
for it in do_not_retry:
|
|
595
603
|
key = (it.id, it.statement)
|
|
@@ -619,35 +627,72 @@ def compile_context(
|
|
|
619
627
|
# costed provisionally here and the difference is charged once it is known
|
|
620
628
|
provisional = _obligations(role, mode, round_no, bool(out.sections["cross_check"]), bool(contract_id), lens)
|
|
621
629
|
overhead = overhead_tokens(out, provisional)
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
for
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
used
|
|
649
|
-
|
|
650
|
-
|
|
630
|
+
def allocate(reserve: int, found: List[Item], fixed: Optional[List[Item]] = None):
|
|
631
|
+
"""(kept, used, left out for the budget): pass one fills each section up to its
|
|
632
|
+
share, the retrieval plugin's ``found`` fill its section up to ``reserve``, and pass
|
|
633
|
+
two fills leftovers in priority order. ``fixed`` is the constraints section as
|
|
634
|
+
compile alone keeps it: the plugin's share is never taken from what always applies
|
|
635
|
+
(the review of ctr_9e166c2f291b829b)."""
|
|
636
|
+
used = overhead
|
|
637
|
+
kept: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
|
|
638
|
+
remaining_by_section: Dict[str, List[Item]] = {name: [] for name in SECTION_ORDER}
|
|
639
|
+
# pass 1: each section up to its share; pass 2: fill leftovers in priority order
|
|
640
|
+
for name in SECTION_ORDER:
|
|
641
|
+
if name == "constraints" and fixed is not None:
|
|
642
|
+
kept[name] = list(fixed)
|
|
643
|
+
used += sum(cost_of(item) for item in fixed)
|
|
644
|
+
held = {id(item) for item in fixed}
|
|
645
|
+
remaining_by_section[name] = [item for item in out.sections[name] if id(item) not in held]
|
|
646
|
+
continue
|
|
647
|
+
cap = int(budget * SECTION_SHARE[name])
|
|
648
|
+
# half of the constraints section is kept for its human-filed items, which arrive
|
|
649
|
+
# in standing order, so the freshly filed ones compete for the other half only
|
|
650
|
+
reserved = int(cap * CONSTRAINT_HUMAN_RESERVE) if name == "constraints" else 0
|
|
651
|
+
section_used = unreserved_used = 0
|
|
652
|
+
for item in out.sections[name]:
|
|
653
|
+
cost = cost_of(item)
|
|
654
|
+
reserve_eligible = bool(reserved) and item.is_human
|
|
655
|
+
fits_share = reserve_eligible or unreserved_used + cost <= cap - reserved
|
|
656
|
+
if section_used + cost <= cap and fits_share and used + cost <= budget - reserve:
|
|
657
|
+
kept[name].append(item)
|
|
658
|
+
section_used += cost
|
|
659
|
+
unreserved_used += 0 if reserve_eligible else cost
|
|
660
|
+
used += cost
|
|
661
|
+
else:
|
|
662
|
+
remaining_by_section[name].append(item)
|
|
663
|
+
taken = {item.id for items in kept.values() for item in items}
|
|
664
|
+
if found:
|
|
665
|
+
# what the plugin found and pass 1 did not already keep, in the plugin's order,
|
|
666
|
+
# up to its share: a record shown once, in the section that placed it first
|
|
667
|
+
kept["retrieved"], spent = [], 0
|
|
668
|
+
for item in found:
|
|
669
|
+
cost = cost_of(item)
|
|
670
|
+
if item.id not in taken and spent + cost <= reserve and used + cost <= budget:
|
|
671
|
+
kept["retrieved"].append(item)
|
|
672
|
+
taken.add(item.id)
|
|
673
|
+
spent += cost
|
|
674
|
+
used += cost
|
|
675
|
+
left_out: List[str] = []
|
|
676
|
+
for name in SECTION_ORDER:
|
|
677
|
+
for item in remaining_by_section[name]:
|
|
678
|
+
if item.id in taken:
|
|
679
|
+
continue # the retrieved section placed it
|
|
680
|
+
cost = cost_of(item)
|
|
681
|
+
if used + cost <= budget:
|
|
682
|
+
kept[name].append(item)
|
|
683
|
+
used += cost
|
|
684
|
+
else:
|
|
685
|
+
left_out.append(item.id)
|
|
686
|
+
return kept, used, left_out
|
|
687
|
+
|
|
688
|
+
# compile alone; and when the retrieval plugin found something, again with its share
|
|
689
|
+
# held back from every section but the constraints, which keep what compile alone
|
|
690
|
+
# gives them. A plugin that finds nothing changes nothing (BENCHMARK-PLAN 1.2)
|
|
691
|
+
kept, used, left_out = allocate(0, [])
|
|
692
|
+
if retrieved:
|
|
693
|
+
kept, used, left_out = allocate(int(budget * retrieval_cfg["share"]), retrieved, fixed=kept["constraints"])
|
|
694
|
+
out.retrieval["ids"] = [item.id for item in kept["retrieved"]]
|
|
695
|
+
out.excluded += [(rid, "budget") for rid in left_out]
|
|
651
696
|
out.sections = kept
|
|
652
697
|
out.used_tokens = used
|
|
653
698
|
|
|
@@ -668,6 +713,55 @@ def compile_context(
|
|
|
668
713
|
return out
|
|
669
714
|
|
|
670
715
|
|
|
716
|
+
def _retrieval(ledger, task: Optional[str], eligible: Dict[str, "Item"], out: "Compiled",
|
|
717
|
+
assessment: Optional[dict] = None):
|
|
718
|
+
"""(the plugin's settings or None, the eligible items it found, best first), and
|
|
719
|
+
``out.retrieval`` set to what the receipt records. A plugin that cannot run leaves the
|
|
720
|
+
view as compile alone makes it, and says so: on the receipt, and in one stderr line."""
|
|
721
|
+
from . import retrieval as retrieval_mod # local: retrieval imports this module
|
|
722
|
+
try:
|
|
723
|
+
cfg = retrieval_mod.settings(ledger.config)
|
|
724
|
+
except retrieval_mod.RetrievalError as exc:
|
|
725
|
+
out.retrieval = {"plugin": retrieval_mod.configured_name(ledger.config), "share": None, "ids": [],
|
|
726
|
+
"error": str(exc)}
|
|
727
|
+
print(f"agent-memory: the retrieval plugin did not run ({exc}); compiled without it", file=sys.stderr)
|
|
728
|
+
return None, []
|
|
729
|
+
if cfg is None:
|
|
730
|
+
return None, []
|
|
731
|
+
out.retrieval = {"plugin": cfg["plugin"], "share": cfg["share"], "ids": []}
|
|
732
|
+
for part in ("embed", "analyse", "restate"): # the configuration a view came from, whether it ran or not
|
|
733
|
+
if part in cfg:
|
|
734
|
+
out.retrieval[part] = dict(cfg[part])
|
|
735
|
+
if not task:
|
|
736
|
+
return cfg, [] # nothing to search by: a view with no task is compile's alone
|
|
737
|
+
report: dict = {}
|
|
738
|
+
|
|
739
|
+
def reported():
|
|
740
|
+
for part in ("analyse", "embed"): # what answered, what it cost, and the scope it chose
|
|
741
|
+
if part in report:
|
|
742
|
+
out.retrieval[part].update(report[part])
|
|
743
|
+
if "withheld" in report: # user-scoped records kept off an endpoint, and sent to one
|
|
744
|
+
out.retrieval["withheld"], out.retrieval["sent_private"] = report["withheld"], report["sent_private"]
|
|
745
|
+
out.retrieval["pending_for"] = report["pending_for"] # which part of the index is behind
|
|
746
|
+
if "error" in report.get("analyse", {}):
|
|
747
|
+
print(f"agent-memory: the retrieval plugin's query analysis did not run ({report['analyse']['error']}); "
|
|
748
|
+
f"searched by the task's own terms", file=sys.stderr)
|
|
749
|
+
try:
|
|
750
|
+
# the reader's policy filter goes into the query, so another reader's records never
|
|
751
|
+
# take this reader's places under the plugin's cuts (an index is never global)
|
|
752
|
+
found, pending = retrieval_mod.retrieve(ledger, task, cfg, assessment, allowed=eligible, report=report)
|
|
753
|
+
if pending is not None: # records BM25 alone could find: this view is not the configured C1's yet
|
|
754
|
+
out.retrieval["pending"] = pending
|
|
755
|
+
except retrieval_mod.RetrievalError as exc:
|
|
756
|
+
reported() # an analysis call made before the failure still cost what it cost
|
|
757
|
+
out.retrieval["error"] = str(exc)
|
|
758
|
+
print(f"agent-memory: the retrieval plugin {cfg['plugin']} did not run ({exc}); compiled without it",
|
|
759
|
+
file=sys.stderr)
|
|
760
|
+
return cfg, []
|
|
761
|
+
reported()
|
|
762
|
+
return cfg, [eligible[rid] for rid in found if rid in eligible]
|
|
763
|
+
|
|
764
|
+
|
|
671
765
|
def compiled_from_json(payload: dict) -> Compiled:
|
|
672
766
|
"""Rebuild a Compiled from ``render.render_json``'s payload.
|
|
673
767
|
|
|
@@ -690,6 +784,7 @@ def compiled_from_json(payload: dict) -> Compiled:
|
|
|
690
784
|
# saying it was cut
|
|
691
785
|
board=[dict(row) for row in payload.get("board") or []],
|
|
692
786
|
)
|
|
787
|
+
out.retrieval = payload.get("retrieval") if isinstance(payload.get("retrieval"), dict) else None
|
|
693
788
|
display = payload.get("reader_display")
|
|
694
789
|
out.reader_display = display if isinstance(display, str) and display else None
|
|
695
790
|
out.config_notes = [n for n in payload.get("config_notes") or [] if isinstance(n, str)]
|
|
@@ -840,6 +935,10 @@ def record_compilation(ledger, compiled: Compiled, rendered: str, render_name: s
|
|
|
840
935
|
# which reliability numbers the reader was shown, or null when annotation was off
|
|
841
936
|
"reliability": compiled.reliability_meta,
|
|
842
937
|
}
|
|
938
|
+
if compiled.retrieval is not None:
|
|
939
|
+
# which configuration produced the view (ctr_9e166c2f291b829b): the plugin, its
|
|
940
|
+
# share, the ids it added, and why it did not run when it did not
|
|
941
|
+
body["retrieval"] = compiled.retrieval
|
|
843
942
|
# which repository the view was cut to, and where it was served and asked from: set
|
|
844
943
|
# only when they are, so a plain view's receipt is what it always was
|
|
845
944
|
for key, value in (("repo", compiled.repo), ("served", compiled.served or None),
|
|
@@ -765,7 +765,8 @@ def run_acceptance(ledger, contract_id: str, cwd: Optional[str] = None, timeout:
|
|
|
765
765
|
detail = (f"exit {body['exit']} (expected {expect_exit})" + (f"; {body.get('summary')}" if body.get("summary") else "")
|
|
766
766
|
+ ("; timed out, and everything it started was stopped" if body.get("timed_out") else ""))
|
|
767
767
|
if sandbox is not None:
|
|
768
|
-
reason = ev.sandbox_failure(sandbox["image"], body, expect_exit, check["cmd"]
|
|
768
|
+
reason = (ev.sandbox_failure(sandbox["image"], body, expect_exit, check["cmd"],
|
|
769
|
+
unproven=ev.image_proof(sandbox, where)) if not ok else None)
|
|
769
770
|
if reason:
|
|
770
771
|
outcome, detail = "error", f"{reason}; {detail}"
|
|
771
772
|
detail = f"sandboxed: podman {sandbox['image']}; {detail}"
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""The endpoint registry's config half (the operator's decision clm_f1fe183b55ed42e5).
|
|
2
|
+
|
|
3
|
+
The model endpoints this machine's components may call are named once, in
|
|
4
|
+
``config.endpoints``, each for the model it serves in short, and a component names the
|
|
5
|
+
one it uses:
|
|
6
|
+
|
|
7
|
+
"endpoints": {
|
|
8
|
+
"Qwen3.5-122B": {"url": "https://node-1.example/v1", "model": "Intel/Qwen3.5-122B-A10B-int4-AutoRound",
|
|
9
|
+
"key_env": "NODE_1_API_KEY", "send_private": true}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
``url`` is an OpenAI-compatible base (``/chat/completions`` and ``/embeddings`` are appended
|
|
13
|
+
to it) and ``model`` the id asked for; what model actually answered is read from each
|
|
14
|
+
response. The key is named, never held: ``key_env`` is an environment variable and
|
|
15
|
+
``key_keychain`` a login-keychain service (the endpoint's name is the item's account), read
|
|
16
|
+
when a call is made and never when the config is checked, so a key is never in a config, an
|
|
17
|
+
argv, a receipt or an error. ``plaintext_key`` lets a key go over ``http://``, as memorize's
|
|
18
|
+
``--llm-insecure`` does. ``send_private`` says the endpoint may receive user-scoped records,
|
|
19
|
+
which no endpoint does by default: it is the endpoint's property (the fleet's own machine,
|
|
20
|
+
yes; a vendor's, no), and each reader of the registry honours it. Over ``http://`` it also
|
|
21
|
+
needs ``plaintext_private``, whatever the key: a key authenticates, and only https hides the
|
|
22
|
+
records in flight; a receipt then says ``plaintext``. ``timeout`` is a call's
|
|
23
|
+
limit in seconds.
|
|
24
|
+
|
|
25
|
+
The retrieval plugin reads the registry first (ctr_9e166c2f291b829b); the memorizer, the
|
|
26
|
+
assessor and the benchmark runners adopt it in their own stories. ``endpoints`` is an
|
|
27
|
+
untrusted-policy key (ledger.UNTRUSTED_POLICY): a clone that named its own would be choosing
|
|
28
|
+
where this machine sends its records, and which secret goes with them.
|
|
29
|
+
|
|
30
|
+
The client opens a socket to the endpoint the config names and nowhere else: it takes no
|
|
31
|
+
proxy from the environment (``HTTP_PROXY`` and its kin), and it follows no redirect, since
|
|
32
|
+
urllib would carry the ``Authorization`` header to wherever a proxy or a redirect pointed.
|
|
33
|
+
It reads a reply only up to MAX_REPLY_BYTES.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import math
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import urllib.error
|
|
42
|
+
import urllib.parse
|
|
43
|
+
import urllib.request
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from typing import Dict, List, Mapping, Optional, Tuple
|
|
46
|
+
|
|
47
|
+
ENTRY_KEYS = ("url", "model", "key_env", "key_keychain", "plaintext_key", "send_private", "plaintext_private",
|
|
48
|
+
"timeout")
|
|
49
|
+
# fields that would hold a secret itself: refused by name, so a key pasted into the config
|
|
50
|
+
# is an error rather than a key committed beside it
|
|
51
|
+
SECRET_FIELDS = ("key", "api_key", "apikey", "token", "bearer", "password", "secret")
|
|
52
|
+
NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$")
|
|
53
|
+
KEY_ENV = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
|
54
|
+
DEFAULT_TIMEOUT = 60.0
|
|
55
|
+
MAX_TIMEOUT = 600.0
|
|
56
|
+
MAX_REPLY_BYTES = 16 * 1024 * 1024 # a completion or a batch of embeddings is far smaller
|
|
57
|
+
DETAIL_CHARS = 300 # of an error reply, quoted in a degraded run's reason
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EndpointError(ValueError):
|
|
61
|
+
"""An entry is malformed, or a call to it failed: the sentence says which, never the key."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Endpoint:
|
|
66
|
+
name: str
|
|
67
|
+
url: str
|
|
68
|
+
model: str
|
|
69
|
+
key_env: Optional[str] = None
|
|
70
|
+
key_keychain: Optional[str] = None
|
|
71
|
+
plaintext_key: bool = False
|
|
72
|
+
send_private: bool = False
|
|
73
|
+
plaintext_private: bool = False
|
|
74
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
75
|
+
|
|
76
|
+
def key(self, env: Optional[Mapping[str, str]] = None) -> Optional[str]:
|
|
77
|
+
"""The key this endpoint's entry names, read now; None when it names none. A named
|
|
78
|
+
key that is not there is an error: the config says this endpoint wants one."""
|
|
79
|
+
if self.key_env:
|
|
80
|
+
found = (os.environ if env is None else env).get(self.key_env)
|
|
81
|
+
if not found:
|
|
82
|
+
raise EndpointError(f"endpoint {self.name!r}: its key_env {self.key_env} is not set")
|
|
83
|
+
return found
|
|
84
|
+
if self.key_keychain:
|
|
85
|
+
from .remote import keychain_token # local: the keychain is read only for a call
|
|
86
|
+
found = keychain_token(self.key_keychain, self.name)
|
|
87
|
+
if not found:
|
|
88
|
+
raise EndpointError(f"endpoint {self.name!r}: no login-keychain item with service "
|
|
89
|
+
f"{self.key_keychain} and account {self.name}")
|
|
90
|
+
return found
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def describe(self) -> dict:
|
|
94
|
+
"""What a receipt says of the endpoint: never its key, nor where the key is kept; and
|
|
95
|
+
``plaintext`` when it is sent user-scoped records over http://."""
|
|
96
|
+
out = {"endpoint": self.name, "url": self.url, "model": self.model, "send_private": self.send_private}
|
|
97
|
+
if self.send_private and self.url.startswith("http://"):
|
|
98
|
+
out["plaintext"] = True
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check(name: str, entry) -> Endpoint:
|
|
103
|
+
"""The entry ``name`` as an Endpoint, or EndpointError saying what is wrong with it."""
|
|
104
|
+
if not isinstance(name, str) or not NAME.match(name):
|
|
105
|
+
raise EndpointError(f"an endpoint's name is letters, digits and _.:- (up to 64), not {name!r}")
|
|
106
|
+
if not isinstance(entry, dict):
|
|
107
|
+
raise EndpointError(f"endpoint {name!r} must be an object with url and model")
|
|
108
|
+
secrets = sorted(set(entry) & set(SECRET_FIELDS))
|
|
109
|
+
if secrets:
|
|
110
|
+
raise EndpointError(f"endpoint {name!r}: a key never goes in the config ({', '.join(secrets)}); name "
|
|
111
|
+
f"where it is kept with key_env or key_keychain")
|
|
112
|
+
unknown = sorted(set(entry) - set(ENTRY_KEYS))
|
|
113
|
+
if unknown:
|
|
114
|
+
raise EndpointError(f"endpoint {name!r} has keys the registry does not read: {unknown}")
|
|
115
|
+
url = entry.get("url")
|
|
116
|
+
parts = urllib.parse.urlsplit(url) if isinstance(url, str) else None
|
|
117
|
+
if parts is None or parts.scheme not in ("https", "http") or not parts.hostname:
|
|
118
|
+
raise EndpointError(f"endpoint {name!r}: url must be an http(s) URL, not {url!r}")
|
|
119
|
+
if parts.username is not None or parts.password is not None or parts.query or parts.fragment:
|
|
120
|
+
# a receipt names the url, so nothing that could be a secret may ride in it
|
|
121
|
+
raise EndpointError(f"endpoint {name!r}: url must carry no credentials, query or fragment")
|
|
122
|
+
model = entry.get("model")
|
|
123
|
+
if not isinstance(model, str) or not model.strip():
|
|
124
|
+
raise EndpointError(f"endpoint {name!r}: model is required (the id to ask for)")
|
|
125
|
+
key_env, key_keychain = entry.get("key_env"), entry.get("key_keychain")
|
|
126
|
+
if key_env is not None and key_keychain is not None:
|
|
127
|
+
raise EndpointError(f"endpoint {name!r}: name one key, key_env or key_keychain")
|
|
128
|
+
if key_env is not None and not (isinstance(key_env, str) and KEY_ENV.match(key_env)):
|
|
129
|
+
raise EndpointError(f"endpoint {name!r}: key_env must be an environment variable's name")
|
|
130
|
+
if key_keychain is not None and not (isinstance(key_keychain, str) and key_keychain.strip()):
|
|
131
|
+
raise EndpointError(f"endpoint {name!r}: key_keychain must be a keychain service's name")
|
|
132
|
+
flags = {}
|
|
133
|
+
for flag in ("plaintext_key", "send_private", "plaintext_private"):
|
|
134
|
+
value = entry.get(flag, False)
|
|
135
|
+
if not isinstance(value, bool):
|
|
136
|
+
raise EndpointError(f"endpoint {name!r}: {flag} must be true or false")
|
|
137
|
+
flags[flag] = value
|
|
138
|
+
if (key_env or key_keychain) and parts.scheme == "http" and not flags["plaintext_key"]:
|
|
139
|
+
raise EndpointError(f"endpoint {name!r}: refusing to send its key to a plaintext endpoint ({url}); use "
|
|
140
|
+
f"https, or set plaintext_key if you accept the risk")
|
|
141
|
+
if flags["send_private"] and parts.scheme == "http" and not flags["plaintext_private"]:
|
|
142
|
+
# whatever its key: a key authenticates, and only https hides the records in flight
|
|
143
|
+
raise EndpointError(f"endpoint {name!r}: refusing to send user-scoped records to a plaintext endpoint "
|
|
144
|
+
f"({url}); use https, or set plaintext_private if you accept the risk")
|
|
145
|
+
timeout = entry.get("timeout", DEFAULT_TIMEOUT)
|
|
146
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not 0 < timeout <= MAX_TIMEOUT:
|
|
147
|
+
raise EndpointError(f"endpoint {name!r}: timeout must be seconds in (0, {MAX_TIMEOUT:g}]")
|
|
148
|
+
return Endpoint(name=name, url=url.rstrip("/"), model=model, key_env=key_env, key_keychain=key_keychain,
|
|
149
|
+
timeout=float(timeout), **flags)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def endpoint(config: Optional[dict], name) -> Endpoint:
|
|
153
|
+
"""The endpoint ``name`` from ``config.endpoints``, or EndpointError."""
|
|
154
|
+
table = (config or {}).get("endpoints")
|
|
155
|
+
if table is None:
|
|
156
|
+
table = {}
|
|
157
|
+
if not isinstance(table, dict):
|
|
158
|
+
raise EndpointError("config.endpoints must be an object of endpoints by name")
|
|
159
|
+
if not isinstance(name, str) or name not in table:
|
|
160
|
+
raise EndpointError(f"no endpoint named {name!r} in config.endpoints")
|
|
161
|
+
return check(name, table[name])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
165
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
166
|
+
return None # the 3xx is answered as an error: the key goes nowhere but the named url
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _opener() -> urllib.request.OpenerDirector:
|
|
170
|
+
"""No proxy, which urllib would otherwise take from HTTP_PROXY and its kin (an http one
|
|
171
|
+
reads the Authorization header), and no redirect: a socket to the named url and nowhere
|
|
172
|
+
else. Built for each call, as cheap as the call is not."""
|
|
173
|
+
return urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _scrub(text: str, key: Optional[str]) -> str:
|
|
177
|
+
return text.replace(key, "<key>") if key else text
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def call(ep: Endpoint, path: str, payload: dict, env: Optional[Mapping[str, str]] = None) -> dict:
|
|
181
|
+
"""POST ``payload`` to the endpoint's ``path`` and return the reply's JSON object."""
|
|
182
|
+
key = ep.key(env)
|
|
183
|
+
headers = {"Content-Type": "application/json"}
|
|
184
|
+
if key:
|
|
185
|
+
headers["Authorization"] = f"Bearer {key}"
|
|
186
|
+
req = urllib.request.Request(ep.url + path, data=json.dumps(payload).encode("utf-8"), headers=headers,
|
|
187
|
+
method="POST")
|
|
188
|
+
try:
|
|
189
|
+
with _opener().open(req, timeout=ep.timeout) as resp:
|
|
190
|
+
raw = resp.read(MAX_REPLY_BYTES + 1)
|
|
191
|
+
except urllib.error.HTTPError as exc:
|
|
192
|
+
try:
|
|
193
|
+
detail = exc.read(DETAIL_CHARS * 4).decode("utf-8", "replace")[:DETAIL_CHARS]
|
|
194
|
+
except OSError:
|
|
195
|
+
detail = ""
|
|
196
|
+
raise EndpointError(f"endpoint {ep.name!r} answered {exc.code}: {_scrub(detail, key)}") from None
|
|
197
|
+
except (urllib.error.URLError, OSError, ValueError) as exc: # refused, unreachable, timed out
|
|
198
|
+
raise EndpointError(f"endpoint {ep.name!r} could not be reached ({type(exc).__name__}: "
|
|
199
|
+
f"{_scrub(str(exc), key)[:DETAIL_CHARS]})") from None
|
|
200
|
+
if len(raw) > MAX_REPLY_BYTES:
|
|
201
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with more than {MAX_REPLY_BYTES} bytes")
|
|
202
|
+
try:
|
|
203
|
+
body = json.loads(raw.decode("utf-8"))
|
|
204
|
+
except (UnicodeDecodeError, ValueError):
|
|
205
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with something other than JSON") from None
|
|
206
|
+
if not isinstance(body, dict):
|
|
207
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with JSON that is not an object")
|
|
208
|
+
return body
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def usage(body: dict) -> dict:
|
|
212
|
+
"""The reply's token counts, as numbers only: what the read cost is counted from."""
|
|
213
|
+
found = body.get("usage") if isinstance(body.get("usage"), dict) else {}
|
|
214
|
+
return {k: v for k, v in found.items() if k in ("prompt_tokens", "completion_tokens", "total_tokens")
|
|
215
|
+
and isinstance(v, int) and not isinstance(v, bool) and v >= 0}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def served(body: dict) -> Optional[str]:
|
|
219
|
+
model = body.get("model")
|
|
220
|
+
return model[:200] if isinstance(model, str) and model else None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def chat(ep: Endpoint, messages: List[dict], max_tokens: int, env: Optional[Mapping[str, str]] = None
|
|
224
|
+
) -> Tuple[str, Optional[str], dict]:
|
|
225
|
+
"""(the reply's text, the served model id, its usage) for one chat completion."""
|
|
226
|
+
body = call(ep, "/chat/completions", {"model": ep.model, "messages": messages, "temperature": 0.0,
|
|
227
|
+
"max_tokens": max_tokens, "stream": False}, env)
|
|
228
|
+
try:
|
|
229
|
+
text = body["choices"][0]["message"]["content"]
|
|
230
|
+
except (KeyError, IndexError, TypeError):
|
|
231
|
+
text = None
|
|
232
|
+
if not isinstance(text, str):
|
|
233
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with no message content")
|
|
234
|
+
return text, served(body), usage(body)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def embeddings(ep: Endpoint, texts: List[str], env: Optional[Mapping[str, str]] = None
|
|
238
|
+
) -> Tuple[List[List[float]], Optional[str], dict]:
|
|
239
|
+
"""(one vector per text, in order; the served model id; the usage) from ``/embeddings``."""
|
|
240
|
+
body = call(ep, "/embeddings", {"model": ep.model, "input": list(texts)}, env)
|
|
241
|
+
data = body.get("data")
|
|
242
|
+
if not isinstance(data, list) or len(data) != len(texts):
|
|
243
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with {len(data) if isinstance(data, list) else 'no'} "
|
|
244
|
+
f"embeddings for {len(texts)} texts")
|
|
245
|
+
by_index: Dict[int, List[float]] = {}
|
|
246
|
+
for position, item in enumerate(data):
|
|
247
|
+
index = item.get("index", position) if isinstance(item, dict) else None
|
|
248
|
+
vector = item.get("embedding") if isinstance(item, dict) else None
|
|
249
|
+
if (not isinstance(index, int) or isinstance(index, bool) or not 0 <= index < len(texts)
|
|
250
|
+
or index in by_index or not isinstance(vector, list) or not vector
|
|
251
|
+
or not all(isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x)
|
|
252
|
+
for x in vector)):
|
|
253
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with a malformed embedding")
|
|
254
|
+
by_index[index] = [float(x) for x in vector]
|
|
255
|
+
dims = {len(v) for v in by_index.values()}
|
|
256
|
+
if len(dims) != 1:
|
|
257
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with embeddings of different lengths")
|
|
258
|
+
return [by_index[i] for i in range(len(texts))], served(body), usage(body)
|