claude-smart 0.2.45 → 0.2.47
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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +47 -10
- package/bin/claude-smart.js +535 -23
- package/package.json +22 -3
- package/plugin/.claude-plugin/plugin.json +4 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +27 -6
- package/plugin/commands/clear-all.md +1 -0
- package/plugin/commands/dashboard.md +1 -0
- package/plugin/commands/learn.md +1 -0
- package/plugin/commands/restart.md +1 -0
- package/plugin/commands/show.md +1 -0
- package/plugin/dashboard/app/configure/env/page.tsx +40 -14
- package/plugin/dashboard/app/configure/server/page.tsx +51 -1
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
- package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
- package/plugin/dashboard/lib/reflexio-client.ts +16 -0
- package/plugin/dashboard/lib/status.ts +10 -3
- package/plugin/dashboard/lib/types.ts +18 -6
- package/plugin/hooks/codex-hooks.json +0 -1
- package/plugin/opencode/assistant-buffer.ts +108 -0
- package/plugin/opencode/dist/assistant-buffer.js +96 -0
- package/plugin/opencode/dist/internal.js +12 -0
- package/plugin/opencode/dist/payload.js +85 -0
- package/plugin/opencode/dist/server.mjs +232 -0
- package/plugin/opencode/internal.ts +18 -0
- package/plugin/opencode/package.json +3 -0
- package/plugin/opencode/payload.ts +90 -0
- package/plugin/opencode/server.mts +251 -0
- package/plugin/opencode/tsconfig.json +13 -0
- package/plugin/pyproject.toml +25 -4
- package/plugin/scripts/_lib.sh +5 -1
- package/plugin/scripts/backend-service.sh +22 -2
- package/plugin/scripts/ensure-plugin-root.sh +4 -4
- package/plugin/scripts/hook_entry.sh +1 -1
- package/plugin/scripts/opencode-claude-compat +4 -0
- package/plugin/scripts/opencode-claude-compat.cmd +3 -0
- package/plugin/scripts/opencode-claude-compat.js +225 -0
- package/plugin/scripts/smart-install.sh +17 -9
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +498 -12
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/src/claude_smart/env_config.py +12 -4
- package/plugin/src/claude_smart/events/session_start.py +26 -7
- package/plugin/src/claude_smart/events/stop.py +1 -1
- package/plugin/src/claude_smart/ids.py +1 -1
- package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
- package/plugin/src/claude_smart/runtime.py +7 -1
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +54 -30
- package/plugin/vendor/reflexio/README.md +17 -11
- package/plugin/vendor/reflexio/pyproject.toml +13 -1
- package/plugin/vendor/reflexio/reflexio/README.md +4 -1
- package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
- package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
- package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
- package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
- package/scripts/setup-claude-smart.sh +8 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
- /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
|
@@ -2,11 +2,16 @@ import asyncio
|
|
|
2
2
|
import inspect
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import time
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
8
|
from datetime import UTC, datetime
|
|
8
|
-
from typing import Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
9
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from reflexio.server.extensions import AppContext, CapabilityRegistry
|
|
13
|
+
|
|
14
|
+
from anyio.to_thread import current_default_thread_limiter
|
|
10
15
|
from fastapi import (
|
|
11
16
|
APIRouter,
|
|
12
17
|
BackgroundTasks,
|
|
@@ -52,6 +57,7 @@ from reflexio.models.api_schema.retriever_schema import (
|
|
|
52
57
|
GetEvaluationResultsViewResponse,
|
|
53
58
|
GetInteractionsRequest,
|
|
54
59
|
GetInteractionsViewResponse,
|
|
60
|
+
GetLearningProvenanceRequest,
|
|
55
61
|
GetPlaybookApplicationStatsRequest,
|
|
56
62
|
GetPlaybookApplicationStatsResponse,
|
|
57
63
|
GetProfileStatisticsResponse,
|
|
@@ -61,6 +67,7 @@ from reflexio.models.api_schema.retriever_schema import (
|
|
|
61
67
|
GetUserPlaybooksRequest,
|
|
62
68
|
GetUserPlaybooksViewResponse,
|
|
63
69
|
GetUserProfilesRequest,
|
|
70
|
+
LearningProvenanceViewResponse,
|
|
64
71
|
ProfileChangeLogViewResponse,
|
|
65
72
|
RequestDataView,
|
|
66
73
|
RerankUserProfilesRequest,
|
|
@@ -74,6 +81,7 @@ from reflexio.models.api_schema.retriever_schema import (
|
|
|
74
81
|
SearchUserProfileRequest,
|
|
75
82
|
SessionView,
|
|
76
83
|
SetConfigResponse,
|
|
84
|
+
SourceUserPlaybookProvenanceView,
|
|
77
85
|
StorageStatsRequest,
|
|
78
86
|
StorageStatsResponse,
|
|
79
87
|
UnifiedSearchRequest,
|
|
@@ -153,10 +161,10 @@ from reflexio.models.api_schema.ui.converters import (
|
|
|
153
161
|
to_user_playbook_view,
|
|
154
162
|
)
|
|
155
163
|
from reflexio.models.config_schema import (
|
|
164
|
+
DEFAULT_WINDOW_SIZE,
|
|
156
165
|
SINGLETON_AGENT_SUCCESS_EVALUATION_NAME,
|
|
157
166
|
Config,
|
|
158
167
|
)
|
|
159
|
-
from reflexio.server._auth import DEFAULT_ORG_ID, default_get_org_id
|
|
160
168
|
from reflexio.server.api_endpoints import (
|
|
161
169
|
account_api,
|
|
162
170
|
health_api,
|
|
@@ -164,6 +172,12 @@ from reflexio.server.api_endpoints import (
|
|
|
164
172
|
publisher_api,
|
|
165
173
|
stall_state_api,
|
|
166
174
|
)
|
|
175
|
+
from reflexio.server.auth import (
|
|
176
|
+
DEFAULT_ORG_ID,
|
|
177
|
+
default_billing_gate,
|
|
178
|
+
default_get_caller_type,
|
|
179
|
+
default_get_org_id,
|
|
180
|
+
)
|
|
167
181
|
from reflexio.server.cache.reflexio_cache import (
|
|
168
182
|
get_reflexio,
|
|
169
183
|
invalidate_reflexio_cache,
|
|
@@ -172,22 +186,34 @@ from reflexio.server.correlation import correlation_id_var, generate_correlation
|
|
|
172
186
|
from reflexio.server.operation_limiter import (
|
|
173
187
|
OperationName,
|
|
174
188
|
limiter_http_exception,
|
|
189
|
+
log_publish_hardware_capacity,
|
|
175
190
|
run_with_operation_limit,
|
|
176
191
|
)
|
|
177
|
-
from reflexio.server.services.agent_success_evaluation.group_evaluation_runner import (
|
|
178
|
-
run_group_evaluation,
|
|
179
|
-
)
|
|
180
192
|
from reflexio.server.services.agent_success_evaluation.regen_jobs import (
|
|
181
193
|
REGEN_JOBS,
|
|
182
194
|
run_regen,
|
|
183
195
|
)
|
|
196
|
+
from reflexio.server.services.agent_success_evaluation.runner import (
|
|
197
|
+
run_group_evaluation,
|
|
198
|
+
)
|
|
199
|
+
from reflexio.server.services.extractor_interaction_utils import (
|
|
200
|
+
get_effective_source_filter,
|
|
201
|
+
get_extractor_window_params,
|
|
202
|
+
)
|
|
203
|
+
from reflexio.server.tracing import profile_step
|
|
184
204
|
|
|
185
205
|
logger = logging.getLogger(__name__)
|
|
186
206
|
|
|
187
207
|
# Re-exported for backwards compatibility — callers that did
|
|
188
208
|
# ``from reflexio.server.api import default_get_org_id`` or ``DEFAULT_ORG_ID``
|
|
189
209
|
# continue to work.
|
|
190
|
-
__all__ = [
|
|
210
|
+
__all__ = [
|
|
211
|
+
"DEFAULT_ORG_ID",
|
|
212
|
+
"create_app",
|
|
213
|
+
"default_billing_gate",
|
|
214
|
+
"default_get_caller_type",
|
|
215
|
+
"default_get_org_id",
|
|
216
|
+
]
|
|
191
217
|
|
|
192
218
|
# Bot protection configuration
|
|
193
219
|
REQUEST_TIMEOUT_SECONDS = 60
|
|
@@ -255,8 +281,41 @@ def get_rate_limit_key(request: Request) -> str:
|
|
|
255
281
|
return get_remote_address(request)
|
|
256
282
|
|
|
257
283
|
|
|
284
|
+
def _storage_backend_name(limiter_obj: Limiter) -> str:
|
|
285
|
+
storage = getattr(limiter_obj, "_storage", None)
|
|
286
|
+
if storage is None:
|
|
287
|
+
return "unknown"
|
|
288
|
+
return storage.__class__.__name__
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _trace_external_rate_limit_backend(limiter_obj: Limiter) -> None:
|
|
292
|
+
"""Trace rate-limit storage hits when the backend is an external service."""
|
|
293
|
+
backend = _storage_backend_name(limiter_obj)
|
|
294
|
+
if backend == "MemoryStorage":
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
strategy = getattr(limiter_obj, "limiter", None)
|
|
298
|
+
if strategy is None or getattr(strategy, "_reflexio_traced", False):
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
original_hit = strategy.hit
|
|
302
|
+
|
|
303
|
+
def traced_hit(item: Any, *identifiers: str, cost: int = 1) -> bool:
|
|
304
|
+
with profile_step(
|
|
305
|
+
"rate_limit.backend_hit",
|
|
306
|
+
storage_backend=backend,
|
|
307
|
+
strategy=strategy.__class__.__name__,
|
|
308
|
+
cost=cost,
|
|
309
|
+
):
|
|
310
|
+
return original_hit(item, *identifiers, cost=cost)
|
|
311
|
+
|
|
312
|
+
strategy.hit = traced_hit
|
|
313
|
+
strategy._reflexio_traced = True
|
|
314
|
+
|
|
315
|
+
|
|
258
316
|
# Initialize rate limiter
|
|
259
317
|
limiter = Limiter(key_func=get_rate_limit_key)
|
|
318
|
+
_trace_external_rate_limit_backend(limiter)
|
|
260
319
|
|
|
261
320
|
|
|
262
321
|
def _run_limited_api[T](
|
|
@@ -448,6 +507,16 @@ class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
|
448
507
|
) -> Response:
|
|
449
508
|
cid = generate_correlation_id()
|
|
450
509
|
correlation_id_var.set(cid)
|
|
510
|
+
try:
|
|
511
|
+
stats = current_default_thread_limiter().statistics()
|
|
512
|
+
request.state.tp_borrowed = stats.borrowed_tokens
|
|
513
|
+
request.state.tp_total = stats.total_tokens
|
|
514
|
+
request.state.tp_waiting = stats.tasks_waiting
|
|
515
|
+
except Exception as exc: # noqa: BLE001
|
|
516
|
+
logger.debug("Failed to snapshot threadpool limiter stats: %s", exc)
|
|
517
|
+
request.state.tp_borrowed = None
|
|
518
|
+
request.state.tp_total = None
|
|
519
|
+
request.state.tp_waiting = None
|
|
451
520
|
response = await call_next(request)
|
|
452
521
|
response.headers["X-Correlation-ID"] = cid
|
|
453
522
|
return response
|
|
@@ -456,6 +525,86 @@ class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
|
456
525
|
core_router = APIRouter()
|
|
457
526
|
|
|
458
527
|
|
|
528
|
+
def _stamp_search_dependencies_done(request: Request) -> None:
|
|
529
|
+
"""Stamp when dependency resolution reaches its final search dependency."""
|
|
530
|
+
request.state.search_deps_done_monotonic = time.monotonic()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _meter_applied_learnings(
|
|
534
|
+
*,
|
|
535
|
+
org_id: str,
|
|
536
|
+
caller_type: str,
|
|
537
|
+
surfaced_count: int,
|
|
538
|
+
request_id: str | None = None,
|
|
539
|
+
session_id: str | None = None,
|
|
540
|
+
) -> None:
|
|
541
|
+
"""Emit the ③ Application event via the OSS emission helper.
|
|
542
|
+
|
|
543
|
+
No-op unless a production-agent caller surfaced >= 1 result. The cheap
|
|
544
|
+
caller-type and count guard runs first so the get_reflexio / config lookup
|
|
545
|
+
is skipped on the free paths (dashboard / empty result).
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
org_id: Organization ID for the requesting caller.
|
|
549
|
+
caller_type: Resolved caller classification (e.g. ``"production_agent"``).
|
|
550
|
+
surfaced_count: Total number of learnings returned to the caller.
|
|
551
|
+
request_id: Optional request correlation ID from the payload.
|
|
552
|
+
session_id: Optional session ID from the payload.
|
|
553
|
+
"""
|
|
554
|
+
if caller_type != "production_agent" or surfaced_count <= 0:
|
|
555
|
+
return
|
|
556
|
+
try:
|
|
557
|
+
from reflexio.server.billing_meter import record_applied_learnings
|
|
558
|
+
from reflexio.server.billing_signals import platform_llm_from_config
|
|
559
|
+
|
|
560
|
+
config = get_reflexio(org_id=org_id).request_context.configurator.get_config()
|
|
561
|
+
record_applied_learnings(
|
|
562
|
+
org_id=org_id,
|
|
563
|
+
surfaced_count=surfaced_count,
|
|
564
|
+
caller_type=caller_type,
|
|
565
|
+
platform_llm=platform_llm_from_config(config),
|
|
566
|
+
platform_storage=None, # resolved enterprise-side at rollup (Phase 1)
|
|
567
|
+
request_id=request_id,
|
|
568
|
+
session_id=session_id,
|
|
569
|
+
)
|
|
570
|
+
except Exception:
|
|
571
|
+
logger.warning(
|
|
572
|
+
"applied-learnings metering failed for org %s", org_id, exc_info=True
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _meter_search_request(
|
|
577
|
+
*,
|
|
578
|
+
org_id: str,
|
|
579
|
+
caller_type: str,
|
|
580
|
+
request_id: str | None = None,
|
|
581
|
+
session_id: str | None = None,
|
|
582
|
+
) -> None:
|
|
583
|
+
"""Emit one production-agent search request metric.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
org_id: Organization ID for the requesting caller.
|
|
587
|
+
caller_type: Resolved caller classification.
|
|
588
|
+
request_id: Optional request correlation ID from the payload.
|
|
589
|
+
session_id: Optional session ID from the payload.
|
|
590
|
+
"""
|
|
591
|
+
if caller_type != "production_agent":
|
|
592
|
+
return
|
|
593
|
+
try:
|
|
594
|
+
from reflexio.server.billing_meter import record_search_request
|
|
595
|
+
|
|
596
|
+
record_search_request(
|
|
597
|
+
org_id=org_id,
|
|
598
|
+
caller_type=caller_type,
|
|
599
|
+
request_id=request_id,
|
|
600
|
+
session_id=session_id,
|
|
601
|
+
)
|
|
602
|
+
except Exception:
|
|
603
|
+
logger.warning(
|
|
604
|
+
"search-request metering failed for org %s", org_id, exc_info=True
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
459
608
|
@core_router.get("/")
|
|
460
609
|
def root() -> dict[str, str]:
|
|
461
610
|
return {
|
|
@@ -466,7 +615,7 @@ def root() -> dict[str, str]:
|
|
|
466
615
|
|
|
467
616
|
|
|
468
617
|
@core_router.get("/health")
|
|
469
|
-
def health_check() -> dict[str, str]:
|
|
618
|
+
async def health_check() -> dict[str, str]:
|
|
470
619
|
"""Health check endpoint for ECS/container orchestration."""
|
|
471
620
|
return {"status": "healthy"}
|
|
472
621
|
|
|
@@ -534,6 +683,7 @@ def publish_user_interaction(
|
|
|
534
683
|
background_tasks: BackgroundTasks,
|
|
535
684
|
org_id: str = Depends(default_get_org_id),
|
|
536
685
|
wait_for_response: bool = False,
|
|
686
|
+
_gate: None = Depends(default_billing_gate("learnings_generated")), # noqa: B008
|
|
537
687
|
) -> PublishUserInteractionResponse:
|
|
538
688
|
if wait_for_response:
|
|
539
689
|
# Process synchronously so the caller gets the real result
|
|
@@ -551,6 +701,7 @@ def publish_user_interaction(
|
|
|
551
701
|
fn=lambda: publisher_api.add_user_interaction(
|
|
552
702
|
org_id=org_id, request=payload
|
|
553
703
|
),
|
|
704
|
+
wait_forever=False,
|
|
554
705
|
)
|
|
555
706
|
except TimeoutError:
|
|
556
707
|
logger.warning(
|
|
@@ -647,17 +798,33 @@ def search_user_profiles(
|
|
|
647
798
|
request: Request,
|
|
648
799
|
payload: SearchUserProfileRequest,
|
|
649
800
|
org_id: str = Depends(default_get_org_id),
|
|
801
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
802
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
650
803
|
) -> SearchProfilesViewResponse:
|
|
651
804
|
response = _run_limited_api(
|
|
652
805
|
org_id,
|
|
653
806
|
"search",
|
|
654
807
|
lambda: get_reflexio(org_id=org_id).search_user_profiles(payload),
|
|
655
808
|
)
|
|
656
|
-
|
|
809
|
+
resp = SearchProfilesViewResponse(
|
|
657
810
|
success=response.success,
|
|
658
811
|
user_profiles=[to_profile_view(p) for p in response.user_profiles],
|
|
659
812
|
msg=response.msg,
|
|
660
813
|
)
|
|
814
|
+
_meter_search_request(
|
|
815
|
+
org_id=org_id,
|
|
816
|
+
caller_type=caller_type,
|
|
817
|
+
request_id=getattr(payload, "request_id", None),
|
|
818
|
+
session_id=getattr(payload, "session_id", None),
|
|
819
|
+
)
|
|
820
|
+
_meter_applied_learnings(
|
|
821
|
+
org_id=org_id,
|
|
822
|
+
caller_type=caller_type,
|
|
823
|
+
surfaced_count=len(resp.user_profiles),
|
|
824
|
+
request_id=getattr(payload, "request_id", None),
|
|
825
|
+
session_id=getattr(payload, "session_id", None),
|
|
826
|
+
)
|
|
827
|
+
return resp
|
|
661
828
|
|
|
662
829
|
|
|
663
830
|
@core_router.post(
|
|
@@ -757,6 +924,8 @@ def search_user_playbooks_endpoint(
|
|
|
757
924
|
request: Request,
|
|
758
925
|
payload: SearchUserPlaybookRequest,
|
|
759
926
|
org_id: str = Depends(default_get_org_id),
|
|
927
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
928
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
760
929
|
) -> SearchUserPlaybooksViewResponse:
|
|
761
930
|
"""Search user playbooks with semantic search and advanced filtering.
|
|
762
931
|
|
|
@@ -767,6 +936,7 @@ def search_user_playbooks_endpoint(
|
|
|
767
936
|
request (Request): The HTTP request object (for rate limiting)
|
|
768
937
|
payload (SearchUserPlaybookRequest): The search request
|
|
769
938
|
org_id (str): Organization ID
|
|
939
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
770
940
|
|
|
771
941
|
Returns:
|
|
772
942
|
SearchUserPlaybooksViewResponse: Response containing matching user playbooks
|
|
@@ -776,11 +946,25 @@ def search_user_playbooks_endpoint(
|
|
|
776
946
|
"search",
|
|
777
947
|
lambda: get_reflexio(org_id=org_id).search_user_playbooks(payload),
|
|
778
948
|
)
|
|
779
|
-
|
|
949
|
+
resp = SearchUserPlaybooksViewResponse(
|
|
780
950
|
success=response.success,
|
|
781
951
|
user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
|
|
782
952
|
msg=response.msg,
|
|
783
953
|
)
|
|
954
|
+
_meter_search_request(
|
|
955
|
+
org_id=org_id,
|
|
956
|
+
caller_type=caller_type,
|
|
957
|
+
request_id=getattr(payload, "request_id", None),
|
|
958
|
+
session_id=getattr(payload, "session_id", None),
|
|
959
|
+
)
|
|
960
|
+
_meter_applied_learnings(
|
|
961
|
+
org_id=org_id,
|
|
962
|
+
caller_type=caller_type,
|
|
963
|
+
surfaced_count=len(resp.user_playbooks),
|
|
964
|
+
request_id=getattr(payload, "request_id", None),
|
|
965
|
+
session_id=getattr(payload, "session_id", None),
|
|
966
|
+
)
|
|
967
|
+
return resp
|
|
784
968
|
|
|
785
969
|
|
|
786
970
|
@core_router.post(
|
|
@@ -793,6 +977,8 @@ def search_agent_playbooks_endpoint(
|
|
|
793
977
|
request: Request,
|
|
794
978
|
payload: SearchAgentPlaybookRequest,
|
|
795
979
|
org_id: str = Depends(default_get_org_id),
|
|
980
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
981
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
796
982
|
) -> SearchAgentPlaybooksViewResponse:
|
|
797
983
|
"""Search agent playbooks with semantic search and advanced filtering.
|
|
798
984
|
|
|
@@ -803,6 +989,7 @@ def search_agent_playbooks_endpoint(
|
|
|
803
989
|
request (Request): The HTTP request object (for rate limiting)
|
|
804
990
|
payload (SearchAgentPlaybookRequest): The search request
|
|
805
991
|
org_id (str): Organization ID
|
|
992
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
806
993
|
|
|
807
994
|
Returns:
|
|
808
995
|
SearchAgentPlaybooksViewResponse: Response containing matching agent playbooks
|
|
@@ -812,11 +999,25 @@ def search_agent_playbooks_endpoint(
|
|
|
812
999
|
"search",
|
|
813
1000
|
lambda: get_reflexio(org_id=org_id).search_agent_playbooks(payload),
|
|
814
1001
|
)
|
|
815
|
-
|
|
1002
|
+
resp = SearchAgentPlaybooksViewResponse(
|
|
816
1003
|
success=response.success,
|
|
817
1004
|
agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
|
|
818
1005
|
msg=response.msg,
|
|
819
1006
|
)
|
|
1007
|
+
_meter_search_request(
|
|
1008
|
+
org_id=org_id,
|
|
1009
|
+
caller_type=caller_type,
|
|
1010
|
+
request_id=getattr(payload, "request_id", None),
|
|
1011
|
+
session_id=getattr(payload, "session_id", None),
|
|
1012
|
+
)
|
|
1013
|
+
_meter_applied_learnings(
|
|
1014
|
+
org_id=org_id,
|
|
1015
|
+
caller_type=caller_type,
|
|
1016
|
+
surfaced_count=len(resp.agent_playbooks),
|
|
1017
|
+
request_id=getattr(payload, "request_id", None),
|
|
1018
|
+
session_id=getattr(payload, "session_id", None),
|
|
1019
|
+
)
|
|
1020
|
+
return resp
|
|
820
1021
|
|
|
821
1022
|
|
|
822
1023
|
@core_router.post(
|
|
@@ -828,7 +1029,11 @@ def search_agent_playbooks_endpoint(
|
|
|
828
1029
|
def unified_search_endpoint(
|
|
829
1030
|
request: Request,
|
|
830
1031
|
payload: UnifiedSearchRequest,
|
|
1032
|
+
background_tasks: BackgroundTasks,
|
|
831
1033
|
org_id: str = Depends(default_get_org_id),
|
|
1034
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
1035
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
1036
|
+
_deps_done: None = Depends(_stamp_search_dependencies_done),
|
|
832
1037
|
) -> UnifiedSearchViewResponse:
|
|
833
1038
|
"""Search across all entity types (profiles, agent playbooks, user playbooks).
|
|
834
1039
|
|
|
@@ -840,31 +1045,76 @@ def unified_search_endpoint(
|
|
|
840
1045
|
request (Request): The HTTP request object (for rate limiting)
|
|
841
1046
|
payload (UnifiedSearchRequest): The unified search request
|
|
842
1047
|
org_id (str): Organization ID
|
|
1048
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
843
1049
|
|
|
844
1050
|
Returns:
|
|
845
1051
|
UnifiedSearchViewResponse: Combined search results
|
|
846
1052
|
"""
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
lambda: get_reflexio(org_id=org_id).unified_search(payload, org_id=org_id),
|
|
851
|
-
)
|
|
852
|
-
return UnifiedSearchViewResponse(
|
|
853
|
-
success=response.success,
|
|
854
|
-
profiles=[to_profile_view(p) for p in response.profiles],
|
|
855
|
-
agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
|
|
856
|
-
user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
|
|
857
|
-
reformulated_query=response.reformulated_query,
|
|
858
|
-
msg=response.msg,
|
|
859
|
-
agent_trace=response.agent_trace,
|
|
860
|
-
rehydrated_text=response.rehydrated_text,
|
|
1053
|
+
deps_done = getattr(request.state, "search_deps_done_monotonic", None)
|
|
1054
|
+
deps_to_body_ms = (
|
|
1055
|
+
int((time.monotonic() - deps_done) * 1000) if deps_done is not None else None
|
|
861
1056
|
)
|
|
1057
|
+
with profile_step(
|
|
1058
|
+
"search.endpoint",
|
|
1059
|
+
enabled=bool(payload.enable_reformulation),
|
|
1060
|
+
has_conversation_history=bool(payload.conversation_history),
|
|
1061
|
+
search_mode=payload.search_mode,
|
|
1062
|
+
) as endpoint_span:
|
|
1063
|
+
endpoint_span.set_data("deps_to_body_ms", deps_to_body_ms)
|
|
1064
|
+
endpoint_span.set_data(
|
|
1065
|
+
"tp_borrowed", getattr(request.state, "tp_borrowed", None)
|
|
1066
|
+
)
|
|
1067
|
+
endpoint_span.set_data("tp_total", getattr(request.state, "tp_total", None))
|
|
1068
|
+
endpoint_span.set_data("tp_waiting", getattr(request.state, "tp_waiting", None))
|
|
1069
|
+
|
|
1070
|
+
def run_search() -> Any:
|
|
1071
|
+
with profile_step("search.reflexio_cache"):
|
|
1072
|
+
reflexio = get_reflexio(org_id=org_id)
|
|
1073
|
+
return reflexio.unified_search(payload, org_id=org_id)
|
|
1074
|
+
|
|
1075
|
+
response = _run_limited_api(org_id, "search", run_search)
|
|
1076
|
+
with profile_step("search.response_view"):
|
|
1077
|
+
resp = UnifiedSearchViewResponse(
|
|
1078
|
+
success=response.success,
|
|
1079
|
+
profiles=[to_profile_view(p) for p in response.profiles],
|
|
1080
|
+
agent_playbooks=[
|
|
1081
|
+
to_agent_playbook_view(fb) for fb in response.agent_playbooks
|
|
1082
|
+
],
|
|
1083
|
+
user_playbooks=[
|
|
1084
|
+
to_user_playbook_view(rf) for rf in response.user_playbooks
|
|
1085
|
+
],
|
|
1086
|
+
reformulated_query=response.reformulated_query,
|
|
1087
|
+
msg=response.msg,
|
|
1088
|
+
agent_trace=response.agent_trace,
|
|
1089
|
+
rehydrated_text=response.rehydrated_text,
|
|
1090
|
+
)
|
|
1091
|
+
background_tasks.add_task(
|
|
1092
|
+
_meter_search_request,
|
|
1093
|
+
org_id=org_id,
|
|
1094
|
+
caller_type=caller_type,
|
|
1095
|
+
request_id=getattr(payload, "request_id", None),
|
|
1096
|
+
session_id=getattr(payload, "session_id", None),
|
|
1097
|
+
)
|
|
1098
|
+
background_tasks.add_task(
|
|
1099
|
+
_meter_applied_learnings,
|
|
1100
|
+
org_id=org_id,
|
|
1101
|
+
caller_type=caller_type,
|
|
1102
|
+
surfaced_count=len(resp.profiles)
|
|
1103
|
+
+ len(resp.agent_playbooks)
|
|
1104
|
+
+ len(resp.user_playbooks),
|
|
1105
|
+
request_id=getattr(payload, "request_id", None),
|
|
1106
|
+
session_id=getattr(payload, "session_id", None),
|
|
1107
|
+
)
|
|
1108
|
+
return resp
|
|
862
1109
|
|
|
863
1110
|
|
|
864
1111
|
@core_router.get("/api/profile_change_log", response_model=ProfileChangeLogViewResponse)
|
|
865
1112
|
def get_profile_change_log(
|
|
866
1113
|
org_id: str = Depends(default_get_org_id),
|
|
867
1114
|
) -> ProfileChangeLogViewResponse:
|
|
1115
|
+
# Serves the reconstructed profile change log (rebuilt from lineage events). The
|
|
1116
|
+
# legacy `profile_change_logs` table is no longer written; see
|
|
1117
|
+
# reconstruct_profile_change_log in lib/_profiles.py.
|
|
868
1118
|
response = get_reflexio(org_id=org_id).get_profile_change_logs()
|
|
869
1119
|
return ProfileChangeLogViewResponse(
|
|
870
1120
|
success=response.success,
|
|
@@ -1269,6 +1519,307 @@ def get_requests_endpoint(
|
|
|
1269
1519
|
)
|
|
1270
1520
|
|
|
1271
1521
|
|
|
1522
|
+
def _learning_provenance_error(
|
|
1523
|
+
payload: GetLearningProvenanceRequest,
|
|
1524
|
+
msg: str,
|
|
1525
|
+
) -> LearningProvenanceViewResponse:
|
|
1526
|
+
return LearningProvenanceViewResponse(
|
|
1527
|
+
success=False,
|
|
1528
|
+
target_kind=payload.kind,
|
|
1529
|
+
target_id=payload.id,
|
|
1530
|
+
provenance_status="unavailable",
|
|
1531
|
+
msg=msg,
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
def _parse_learning_target_int(
|
|
1536
|
+
payload: GetLearningProvenanceRequest,
|
|
1537
|
+
) -> int | LearningProvenanceViewResponse:
|
|
1538
|
+
try:
|
|
1539
|
+
return int(payload.id)
|
|
1540
|
+
except ValueError:
|
|
1541
|
+
return _learning_provenance_error(
|
|
1542
|
+
payload,
|
|
1543
|
+
f"{payload.kind} id must be an integer",
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def _sort_interactions_by_time(interactions: list[Any]) -> list[Any]:
|
|
1548
|
+
return sorted(
|
|
1549
|
+
interactions,
|
|
1550
|
+
key=lambda i: (
|
|
1551
|
+
getattr(i, "created_at", 0) or 0,
|
|
1552
|
+
getattr(i, "interaction_id", 0),
|
|
1553
|
+
),
|
|
1554
|
+
)
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
def _interaction_views(interactions: list[Any]) -> list[Any]:
|
|
1558
|
+
return [to_interaction_view(i) for i in _sort_interactions_by_time(interactions)]
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
def _effective_profile_provenance_window(
|
|
1562
|
+
reflexio: Any, trigger_source: str | None
|
|
1563
|
+
) -> tuple[int, list[str] | None]:
|
|
1564
|
+
config = reflexio.request_context.configurator.get_config()
|
|
1565
|
+
profile_config = getattr(config, "profile_extractor_config", None)
|
|
1566
|
+
if profile_config is None:
|
|
1567
|
+
return getattr(config, "window_size", DEFAULT_WINDOW_SIZE), None
|
|
1568
|
+
|
|
1569
|
+
window_size, _ = get_extractor_window_params(
|
|
1570
|
+
profile_config,
|
|
1571
|
+
getattr(config, "window_size", None),
|
|
1572
|
+
getattr(config, "stride_size", None),
|
|
1573
|
+
)
|
|
1574
|
+
should_skip, source_filter = get_effective_source_filter(
|
|
1575
|
+
profile_config,
|
|
1576
|
+
trigger_source,
|
|
1577
|
+
)
|
|
1578
|
+
if should_skip:
|
|
1579
|
+
return window_size, None
|
|
1580
|
+
return window_size, source_filter
|
|
1581
|
+
|
|
1582
|
+
|
|
1583
|
+
def _get_profile_learning_provenance(
|
|
1584
|
+
payload: GetLearningProvenanceRequest,
|
|
1585
|
+
reflexio: Any,
|
|
1586
|
+
) -> LearningProvenanceViewResponse:
|
|
1587
|
+
storage = reflexio.request_context.storage
|
|
1588
|
+
profile = storage.get_profile_by_id(payload.id)
|
|
1589
|
+
if profile is None:
|
|
1590
|
+
return _learning_provenance_error(payload, "Profile not found")
|
|
1591
|
+
|
|
1592
|
+
if profile.source_interaction_ids:
|
|
1593
|
+
interactions = storage.get_interactions_by_ids(profile.source_interaction_ids)
|
|
1594
|
+
return LearningProvenanceViewResponse(
|
|
1595
|
+
success=True,
|
|
1596
|
+
target_kind=payload.kind,
|
|
1597
|
+
target_id=payload.id,
|
|
1598
|
+
provenance_status="exact",
|
|
1599
|
+
trigger_request_id=profile.generated_from_request_id,
|
|
1600
|
+
interactions=_interaction_views(interactions),
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
if not profile.generated_from_request_id:
|
|
1604
|
+
return _learning_provenance_error(
|
|
1605
|
+
payload,
|
|
1606
|
+
"Profile has no generation request for provenance reconstruction",
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
trigger_request = storage.get_request(profile.generated_from_request_id)
|
|
1610
|
+
if trigger_request is None:
|
|
1611
|
+
return _learning_provenance_error(
|
|
1612
|
+
payload,
|
|
1613
|
+
"Profile generation request was not found",
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
trigger_interactions = storage.get_interactions_by_request_ids(
|
|
1617
|
+
[profile.generated_from_request_id]
|
|
1618
|
+
)
|
|
1619
|
+
anchor_time = (
|
|
1620
|
+
max(i.created_at for i in trigger_interactions)
|
|
1621
|
+
if trigger_interactions
|
|
1622
|
+
else trigger_request.created_at
|
|
1623
|
+
)
|
|
1624
|
+
window_size, source_filter = _effective_profile_provenance_window(
|
|
1625
|
+
reflexio,
|
|
1626
|
+
trigger_request.source,
|
|
1627
|
+
)
|
|
1628
|
+
_, interactions = storage.get_last_k_interactions_grouped(
|
|
1629
|
+
user_id=profile.user_id,
|
|
1630
|
+
k=window_size,
|
|
1631
|
+
sources=source_filter,
|
|
1632
|
+
end_time=anchor_time,
|
|
1633
|
+
)
|
|
1634
|
+
return LearningProvenanceViewResponse(
|
|
1635
|
+
success=True,
|
|
1636
|
+
target_kind=payload.kind,
|
|
1637
|
+
target_id=payload.id,
|
|
1638
|
+
provenance_status="best_effort" if interactions else "unavailable",
|
|
1639
|
+
trigger_request_id=profile.generated_from_request_id,
|
|
1640
|
+
interactions=_interaction_views(interactions),
|
|
1641
|
+
msg=None if interactions else "No interactions found for reconstructed window",
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1644
|
+
|
|
1645
|
+
def _get_user_playbook_learning_provenance(
|
|
1646
|
+
payload: GetLearningProvenanceRequest,
|
|
1647
|
+
reflexio: Any,
|
|
1648
|
+
) -> LearningProvenanceViewResponse:
|
|
1649
|
+
parsed_id = _parse_learning_target_int(payload)
|
|
1650
|
+
if isinstance(parsed_id, LearningProvenanceViewResponse):
|
|
1651
|
+
return parsed_id
|
|
1652
|
+
|
|
1653
|
+
storage = reflexio.request_context.storage
|
|
1654
|
+
playbook = storage.get_user_playbook_by_id(parsed_id)
|
|
1655
|
+
if playbook is None:
|
|
1656
|
+
return _learning_provenance_error(payload, "User playbook not found")
|
|
1657
|
+
|
|
1658
|
+
status = "exact"
|
|
1659
|
+
if playbook.source_interaction_ids:
|
|
1660
|
+
interactions = storage.get_interactions_by_ids(playbook.source_interaction_ids)
|
|
1661
|
+
elif playbook.request_id:
|
|
1662
|
+
status = "best_effort"
|
|
1663
|
+
interactions = storage.get_interactions_by_request_ids([playbook.request_id])
|
|
1664
|
+
else:
|
|
1665
|
+
status = "unavailable"
|
|
1666
|
+
interactions = []
|
|
1667
|
+
|
|
1668
|
+
return LearningProvenanceViewResponse(
|
|
1669
|
+
success=True,
|
|
1670
|
+
target_kind=payload.kind,
|
|
1671
|
+
target_id=payload.id,
|
|
1672
|
+
provenance_status=status if interactions else "unavailable",
|
|
1673
|
+
trigger_request_id=playbook.request_id,
|
|
1674
|
+
interactions=_interaction_views(interactions),
|
|
1675
|
+
msg=None if interactions else "No interactions found for this user playbook",
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
def _get_agent_playbook_learning_provenance(
|
|
1680
|
+
payload: GetLearningProvenanceRequest,
|
|
1681
|
+
reflexio: Any,
|
|
1682
|
+
) -> LearningProvenanceViewResponse:
|
|
1683
|
+
parsed_id = _parse_learning_target_int(payload)
|
|
1684
|
+
if isinstance(parsed_id, LearningProvenanceViewResponse):
|
|
1685
|
+
return parsed_id
|
|
1686
|
+
|
|
1687
|
+
storage = reflexio.request_context.storage
|
|
1688
|
+
agent_playbook = storage.get_agent_playbook_by_id(parsed_id)
|
|
1689
|
+
if agent_playbook is None:
|
|
1690
|
+
return _learning_provenance_error(payload, "Agent playbook not found")
|
|
1691
|
+
|
|
1692
|
+
windows = storage.get_source_windows_for_agent_playbook(parsed_id)
|
|
1693
|
+
if not windows:
|
|
1694
|
+
return LearningProvenanceViewResponse(
|
|
1695
|
+
success=True,
|
|
1696
|
+
target_kind=payload.kind,
|
|
1697
|
+
target_id=payload.id,
|
|
1698
|
+
provenance_status="unavailable",
|
|
1699
|
+
msg="No source user playbooks are recorded for this agent playbook",
|
|
1700
|
+
)
|
|
1701
|
+
|
|
1702
|
+
user_playbook_ids = [window.user_playbook_id for window in windows]
|
|
1703
|
+
user_playbooks = storage.get_user_playbooks_by_ids_any_user(
|
|
1704
|
+
user_playbook_ids,
|
|
1705
|
+
status_filter=None,
|
|
1706
|
+
)
|
|
1707
|
+
user_playbooks_by_id = {
|
|
1708
|
+
playbook.user_playbook_id: playbook for playbook in user_playbooks
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
interaction_ids_to_fetch: set[int] = set()
|
|
1712
|
+
request_ids_to_fetch: set[str] = set()
|
|
1713
|
+
group_sources: dict[int, tuple[list[int], str | None, bool]] = {}
|
|
1714
|
+
for window in windows:
|
|
1715
|
+
source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
|
|
1716
|
+
if source_user_playbook is None:
|
|
1717
|
+
continue
|
|
1718
|
+
|
|
1719
|
+
source_ids = list(window.source_interaction_ids)
|
|
1720
|
+
request_id: str | None = None
|
|
1721
|
+
uses_fallback = False
|
|
1722
|
+
if not source_ids and source_user_playbook.source_interaction_ids:
|
|
1723
|
+
source_ids = list(source_user_playbook.source_interaction_ids)
|
|
1724
|
+
elif not source_ids and source_user_playbook.request_id:
|
|
1725
|
+
request_id = source_user_playbook.request_id
|
|
1726
|
+
uses_fallback = True
|
|
1727
|
+
elif not source_ids:
|
|
1728
|
+
uses_fallback = True
|
|
1729
|
+
|
|
1730
|
+
interaction_ids_to_fetch.update(source_ids)
|
|
1731
|
+
if request_id:
|
|
1732
|
+
request_ids_to_fetch.add(request_id)
|
|
1733
|
+
group_sources[window.user_playbook_id] = (source_ids, request_id, uses_fallback)
|
|
1734
|
+
|
|
1735
|
+
interactions_by_id = (
|
|
1736
|
+
{
|
|
1737
|
+
interaction.interaction_id: interaction
|
|
1738
|
+
for interaction in storage.get_interactions_by_ids(
|
|
1739
|
+
sorted(interaction_ids_to_fetch)
|
|
1740
|
+
)
|
|
1741
|
+
}
|
|
1742
|
+
if interaction_ids_to_fetch
|
|
1743
|
+
else {}
|
|
1744
|
+
)
|
|
1745
|
+
request_interactions: dict[str, list[Any]] = {}
|
|
1746
|
+
if request_ids_to_fetch:
|
|
1747
|
+
for interaction in storage.get_interactions_by_request_ids(
|
|
1748
|
+
sorted(request_ids_to_fetch)
|
|
1749
|
+
):
|
|
1750
|
+
request_interactions.setdefault(interaction.request_id, []).append(
|
|
1751
|
+
interaction
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1754
|
+
groups: list[SourceUserPlaybookProvenanceView] = []
|
|
1755
|
+
used_fallback = False
|
|
1756
|
+
found_interactions = False
|
|
1757
|
+
for window in windows:
|
|
1758
|
+
source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
|
|
1759
|
+
if source_user_playbook is None:
|
|
1760
|
+
used_fallback = True
|
|
1761
|
+
continue
|
|
1762
|
+
|
|
1763
|
+
source_ids, request_id, uses_fallback = group_sources.get(
|
|
1764
|
+
window.user_playbook_id,
|
|
1765
|
+
([], None, True),
|
|
1766
|
+
)
|
|
1767
|
+
used_fallback = used_fallback or uses_fallback
|
|
1768
|
+
interactions = (
|
|
1769
|
+
[interactions_by_id[i] for i in source_ids if i in interactions_by_id]
|
|
1770
|
+
if source_ids
|
|
1771
|
+
else request_interactions.get(request_id or "", [])
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
found_interactions = found_interactions or bool(interactions)
|
|
1775
|
+
groups.append(
|
|
1776
|
+
SourceUserPlaybookProvenanceView(
|
|
1777
|
+
user_playbook=to_user_playbook_view(source_user_playbook),
|
|
1778
|
+
interactions=_interaction_views(interactions),
|
|
1779
|
+
source_interaction_ids=source_ids,
|
|
1780
|
+
)
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1783
|
+
if not groups:
|
|
1784
|
+
return _learning_provenance_error(
|
|
1785
|
+
payload,
|
|
1786
|
+
"Recorded source user playbooks were not found",
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1789
|
+
return LearningProvenanceViewResponse(
|
|
1790
|
+
success=True,
|
|
1791
|
+
target_kind=payload.kind,
|
|
1792
|
+
target_id=payload.id,
|
|
1793
|
+
provenance_status=(
|
|
1794
|
+
"exact"
|
|
1795
|
+
if found_interactions and not used_fallback
|
|
1796
|
+
else "best_effort"
|
|
1797
|
+
if found_interactions
|
|
1798
|
+
else "unavailable"
|
|
1799
|
+
),
|
|
1800
|
+
source_user_playbooks=groups,
|
|
1801
|
+
msg=None if found_interactions else "No source interactions were found",
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
@core_router.post(
|
|
1806
|
+
"/api/get_learning_provenance",
|
|
1807
|
+
response_model=LearningProvenanceViewResponse,
|
|
1808
|
+
response_model_exclude_none=True,
|
|
1809
|
+
)
|
|
1810
|
+
def get_learning_provenance(
|
|
1811
|
+
payload: GetLearningProvenanceRequest,
|
|
1812
|
+
org_id: str = Depends(default_get_org_id),
|
|
1813
|
+
) -> LearningProvenanceViewResponse:
|
|
1814
|
+
"""Return read-only learning provenance for a generated learning row."""
|
|
1815
|
+
reflexio = get_reflexio(org_id=org_id)
|
|
1816
|
+
if payload.kind == "profile":
|
|
1817
|
+
return _get_profile_learning_provenance(payload, reflexio)
|
|
1818
|
+
if payload.kind == "user_playbook":
|
|
1819
|
+
return _get_user_playbook_learning_provenance(payload, reflexio)
|
|
1820
|
+
return _get_agent_playbook_learning_provenance(payload, reflexio)
|
|
1821
|
+
|
|
1822
|
+
|
|
1272
1823
|
@core_router.post(
|
|
1273
1824
|
"/api/get_profiles",
|
|
1274
1825
|
response_model=GetProfilesViewResponse,
|
|
@@ -1296,6 +1847,14 @@ def get_all_profiles(
|
|
|
1296
1847
|
request: Request,
|
|
1297
1848
|
limit: int = 100,
|
|
1298
1849
|
status_filter: str | None = None,
|
|
1850
|
+
profile_id: str | None = None,
|
|
1851
|
+
user_id: str | None = None,
|
|
1852
|
+
query: str | None = None,
|
|
1853
|
+
source: str | None = None,
|
|
1854
|
+
profile_time_to_live: str | None = None,
|
|
1855
|
+
start_time: int | None = None,
|
|
1856
|
+
end_time: int | None = None,
|
|
1857
|
+
include_tombstones: bool = False,
|
|
1299
1858
|
org_id: str = Depends(default_get_org_id),
|
|
1300
1859
|
) -> GetProfilesViewResponse:
|
|
1301
1860
|
"""Get all user profiles across all users.
|
|
@@ -1303,6 +1862,15 @@ def get_all_profiles(
|
|
|
1303
1862
|
Args:
|
|
1304
1863
|
limit (int, optional): Maximum number of profiles to return. Defaults to 100.
|
|
1305
1864
|
status_filter (str, optional): Filter by profile status. Can be "current", "pending", or "archived".
|
|
1865
|
+
profile_id (str, optional): Exact profile ID to retrieve.
|
|
1866
|
+
user_id (str, optional): Exact user ID to filter by.
|
|
1867
|
+
query (str, optional): Case-insensitive text filter across visible fields.
|
|
1868
|
+
source (str, optional): Exact profile source to filter by.
|
|
1869
|
+
profile_time_to_live (str, optional): Exact TTL value to filter by.
|
|
1870
|
+
start_time (int, optional): Minimum last-modified epoch seconds.
|
|
1871
|
+
end_time (int, optional): Maximum last-modified epoch seconds.
|
|
1872
|
+
include_tombstones (bool, optional): Include merged/superseded rows when
|
|
1873
|
+
looking up a specific profile_id.
|
|
1306
1874
|
org_id (str): Organization ID
|
|
1307
1875
|
|
|
1308
1876
|
Returns:
|
|
@@ -1310,6 +1878,24 @@ def get_all_profiles(
|
|
|
1310
1878
|
"""
|
|
1311
1879
|
reflexio = get_reflexio(org_id=org_id)
|
|
1312
1880
|
|
|
1881
|
+
if profile_id and include_tombstones:
|
|
1882
|
+
storage = reflexio.request_context.storage
|
|
1883
|
+
if storage is None:
|
|
1884
|
+
return GetProfilesViewResponse(
|
|
1885
|
+
success=True,
|
|
1886
|
+
user_profiles=[],
|
|
1887
|
+
msg="Storage is not configured",
|
|
1888
|
+
)
|
|
1889
|
+
profile = storage.get_profile_by_id(
|
|
1890
|
+
profile_id, include_tombstones=include_tombstones
|
|
1891
|
+
)
|
|
1892
|
+
profiles = [profile] if profile else []
|
|
1893
|
+
return GetProfilesViewResponse(
|
|
1894
|
+
success=True,
|
|
1895
|
+
user_profiles=[to_profile_view(p) for p in profiles],
|
|
1896
|
+
msg=f"Found {len(profiles)} profile(s)",
|
|
1897
|
+
)
|
|
1898
|
+
|
|
1313
1899
|
# Map status_filter string to Status list
|
|
1314
1900
|
status_filter_list = None
|
|
1315
1901
|
if status_filter == "current":
|
|
@@ -1319,7 +1905,17 @@ def get_all_profiles(
|
|
|
1319
1905
|
elif status_filter == "archived":
|
|
1320
1906
|
status_filter_list = [Status.ARCHIVED]
|
|
1321
1907
|
|
|
1322
|
-
response = reflexio.get_all_profiles(
|
|
1908
|
+
response = reflexio.get_all_profiles(
|
|
1909
|
+
limit=limit,
|
|
1910
|
+
status_filter=status_filter_list, # type: ignore[reportArgumentType]
|
|
1911
|
+
user_id=user_id,
|
|
1912
|
+
profile_id=profile_id,
|
|
1913
|
+
query=query,
|
|
1914
|
+
source=source,
|
|
1915
|
+
profile_time_to_live=profile_time_to_live,
|
|
1916
|
+
start_time=start_time,
|
|
1917
|
+
end_time=end_time,
|
|
1918
|
+
)
|
|
1323
1919
|
return GetProfilesViewResponse(
|
|
1324
1920
|
success=response.success,
|
|
1325
1921
|
user_profiles=[to_profile_view(p) for p in response.user_profiles],
|
|
@@ -1378,17 +1974,30 @@ def set_config(
|
|
|
1378
1974
|
"""Set configuration for the organization.
|
|
1379
1975
|
|
|
1380
1976
|
Args:
|
|
1381
|
-
config (
|
|
1977
|
+
config (dict[str, Any]): The configuration payload to set
|
|
1382
1978
|
org_id (str): Organization ID
|
|
1383
1979
|
|
|
1384
1980
|
Returns:
|
|
1385
1981
|
dict: Response containing success status and message
|
|
1386
1982
|
"""
|
|
1983
|
+
from pydantic import ValidationError
|
|
1984
|
+
|
|
1387
1985
|
# Create Reflexio instance to access the configurator through request_context
|
|
1388
1986
|
reflexio = get_reflexio(org_id=org_id)
|
|
1987
|
+
configurator = reflexio.request_context.configurator
|
|
1988
|
+
normalized_config = configurator.normalize_config_payload(config)
|
|
1989
|
+
if not isinstance(normalized_config, dict):
|
|
1990
|
+
normalized_config = config
|
|
1991
|
+
try:
|
|
1992
|
+
Config.model_validate(normalized_config)
|
|
1993
|
+
except ValidationError as exc:
|
|
1994
|
+
raise HTTPException(
|
|
1995
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
1996
|
+
detail=exc.errors(),
|
|
1997
|
+
) from exc
|
|
1389
1998
|
|
|
1390
1999
|
# Set the config using Reflexio's set_config method
|
|
1391
|
-
response = reflexio.set_config(
|
|
2000
|
+
response = reflexio.set_config(normalized_config)
|
|
1392
2001
|
|
|
1393
2002
|
# Invalidate cache on successful config change to ensure fresh instance next request
|
|
1394
2003
|
if response.success:
|
|
@@ -1568,26 +2177,40 @@ def get_user_playbooks(
|
|
|
1568
2177
|
response_model=GetAgentPlaybooksViewResponse,
|
|
1569
2178
|
response_model_exclude_none=True,
|
|
1570
2179
|
)
|
|
2180
|
+
@limiter.limit("120/minute") # Rate limit for read operations
|
|
1571
2181
|
def get_agent_playbooks(
|
|
1572
|
-
request:
|
|
2182
|
+
request: Request,
|
|
2183
|
+
payload: GetAgentPlaybooksRequest,
|
|
1573
2184
|
org_id: str = Depends(default_get_org_id),
|
|
2185
|
+
caller_type: str = Depends(default_get_caller_type),
|
|
2186
|
+
_gate: None = Depends(default_billing_gate("application")), # noqa: B008
|
|
1574
2187
|
) -> GetAgentPlaybooksViewResponse:
|
|
1575
2188
|
"""Get agent playbooks with internal fields filtered out.
|
|
1576
2189
|
|
|
1577
2190
|
Args:
|
|
1578
|
-
request (
|
|
2191
|
+
request (Request): The HTTP request object (for rate limiting)
|
|
2192
|
+
payload (GetAgentPlaybooksRequest): The get request
|
|
1579
2193
|
org_id (str): Organization ID
|
|
2194
|
+
caller_type (str): Billing caller classification (injected via dependency).
|
|
1580
2195
|
|
|
1581
2196
|
Returns:
|
|
1582
2197
|
GetAgentPlaybooksViewResponse: Response containing agent playbooks without internal fields
|
|
1583
2198
|
"""
|
|
1584
2199
|
reflexio = get_reflexio(org_id=org_id)
|
|
1585
|
-
response = reflexio.get_agent_playbooks(
|
|
1586
|
-
|
|
2200
|
+
response = reflexio.get_agent_playbooks(payload)
|
|
2201
|
+
resp = GetAgentPlaybooksViewResponse(
|
|
1587
2202
|
success=response.success,
|
|
1588
2203
|
agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
|
|
1589
2204
|
msg=response.msg,
|
|
1590
2205
|
)
|
|
2206
|
+
_meter_applied_learnings(
|
|
2207
|
+
org_id=org_id,
|
|
2208
|
+
caller_type=caller_type,
|
|
2209
|
+
surfaced_count=len(resp.agent_playbooks),
|
|
2210
|
+
request_id=getattr(payload, "request_id", None),
|
|
2211
|
+
session_id=getattr(payload, "session_id", None),
|
|
2212
|
+
)
|
|
2213
|
+
return resp
|
|
1591
2214
|
|
|
1592
2215
|
|
|
1593
2216
|
@core_router.post(
|
|
@@ -2094,10 +2717,8 @@ def _read_grade_on_demand_cache(
|
|
|
2094
2717
|
def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
|
|
2095
2718
|
"""Look up the user_id that owns a session_id without requiring it as input.
|
|
2096
2719
|
|
|
2097
|
-
Uses
|
|
2098
|
-
|
|
2099
|
-
request-fetcher requires a user_id paired with it. Returns None when
|
|
2100
|
-
the session has no requests, signalling NO_REQUESTS to the caller.
|
|
2720
|
+
Uses the first-request bulk helper even for this single-session path so the
|
|
2721
|
+
lookup can use the same indexed query shape as evaluation overview.
|
|
2101
2722
|
|
|
2102
2723
|
Args:
|
|
2103
2724
|
storage: The request's storage backend.
|
|
@@ -2107,17 +2728,17 @@ def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
|
|
|
2107
2728
|
str | None: The user_id of the earliest request in the session,
|
|
2108
2729
|
or None when no requests exist.
|
|
2109
2730
|
"""
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
if
|
|
2731
|
+
first_requests = storage.get_first_requests_by_session_ids([session_id])
|
|
2732
|
+
first = first_requests.get(session_id)
|
|
2733
|
+
if first is None:
|
|
2113
2734
|
return None
|
|
2114
|
-
|
|
2115
|
-
return earliest.request.user_id
|
|
2735
|
+
return first.user_id
|
|
2116
2736
|
|
|
2117
2737
|
|
|
2118
2738
|
def _find_fresh_result_id(
|
|
2119
2739
|
storage: Any,
|
|
2120
2740
|
*,
|
|
2741
|
+
user_id: str,
|
|
2121
2742
|
session_id: str,
|
|
2122
2743
|
agent_version: str,
|
|
2123
2744
|
evaluation_name: str,
|
|
@@ -2125,12 +2746,12 @@ def _find_fresh_result_id(
|
|
|
2125
2746
|
) -> int | None:
|
|
2126
2747
|
"""Locate the result_id written by the most-recent grade for this session.
|
|
2127
2748
|
|
|
2128
|
-
The runner writes rows but doesn't return the id.
|
|
2129
|
-
|
|
2130
|
-
the regen worker's prior-row capture (group_evaluation_runner step 5).
|
|
2749
|
+
The runner writes rows but doesn't return the id. Use the targeted result-id
|
|
2750
|
+
lookup so this path does not scan broad evaluation windows.
|
|
2131
2751
|
|
|
2132
2752
|
Args:
|
|
2133
2753
|
storage: The request's storage backend.
|
|
2754
|
+
user_id (str): The user whose session slice was graded.
|
|
2134
2755
|
session_id (str): The graded session.
|
|
2135
2756
|
agent_version (str): The version dimension.
|
|
2136
2757
|
evaluation_name (str): Evaluator/result namespace to isolate readback.
|
|
@@ -2140,20 +2761,16 @@ def _find_fresh_result_id(
|
|
|
2140
2761
|
int | None: result_id of the latest matching row, or None if the
|
|
2141
2762
|
runner wrote nothing.
|
|
2142
2763
|
"""
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
and r.result_id not in previous_result_ids
|
|
2152
|
-
]
|
|
2153
|
-
if not matched:
|
|
2764
|
+
result_ids = storage.get_agent_success_evaluation_result_ids(
|
|
2765
|
+
user_id=user_id,
|
|
2766
|
+
session_id=session_id,
|
|
2767
|
+
evaluation_name=evaluation_name,
|
|
2768
|
+
agent_version=agent_version,
|
|
2769
|
+
)
|
|
2770
|
+
fresh_result_ids = [rid for rid in result_ids if rid not in previous_result_ids]
|
|
2771
|
+
if not fresh_result_ids:
|
|
2154
2772
|
return None
|
|
2155
|
-
|
|
2156
|
-
return latest.result_id
|
|
2773
|
+
return max(fresh_result_ids)
|
|
2157
2774
|
|
|
2158
2775
|
|
|
2159
2776
|
@core_router.post(
|
|
@@ -2224,13 +2841,14 @@ def grade_on_demand(
|
|
|
2224
2841
|
skipped_reason="NO_REQUESTS",
|
|
2225
2842
|
)
|
|
2226
2843
|
|
|
2227
|
-
previous_result_ids =
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2844
|
+
previous_result_ids = set(
|
|
2845
|
+
storage.get_agent_success_evaluation_result_ids(
|
|
2846
|
+
user_id=user_id,
|
|
2847
|
+
session_id=payload.session_id,
|
|
2848
|
+
evaluation_name=evaluation_name,
|
|
2849
|
+
agent_version=payload.agent_version,
|
|
2231
2850
|
)
|
|
2232
|
-
|
|
2233
|
-
}
|
|
2851
|
+
)
|
|
2234
2852
|
|
|
2235
2853
|
# Two operation_state rows are intentionally written for this session:
|
|
2236
2854
|
# 1) `grade_on_demand::{org_id}::{session_id}::{agent_version}::{evaluation_name}`
|
|
@@ -2255,6 +2873,7 @@ def grade_on_demand(
|
|
|
2255
2873
|
|
|
2256
2874
|
result_id = _find_fresh_result_id(
|
|
2257
2875
|
storage,
|
|
2876
|
+
user_id=user_id,
|
|
2258
2877
|
session_id=payload.session_id,
|
|
2259
2878
|
agent_version=payload.agent_version,
|
|
2260
2879
|
evaluation_name=evaluation_name,
|
|
@@ -2307,8 +2926,8 @@ def get_recent_shadow_comparisons(
|
|
|
2307
2926
|
Filters to the org's currently pinned
|
|
2308
2927
|
``Config.shadow_comparison_judge_prompt_version`` so verdicts produced
|
|
2309
2928
|
under an older rubric do not mix into the drawer or the Top 10
|
|
2310
|
-
disagreements widget. Storage returns verdicts
|
|
2311
|
-
|
|
2929
|
+
disagreements widget. Storage returns verdicts newest-first and caps the
|
|
2930
|
+
read at ``limit``.
|
|
2312
2931
|
|
|
2313
2932
|
Args:
|
|
2314
2933
|
limit (int): Max verdicts to return. Clamped to ``[1, 100]``.
|
|
@@ -2340,10 +2959,11 @@ def get_recent_shadow_comparisons(
|
|
|
2340
2959
|
|
|
2341
2960
|
now = int(datetime.now(UTC).timestamp())
|
|
2342
2961
|
try:
|
|
2343
|
-
verdicts = storage.
|
|
2962
|
+
verdicts = storage.get_recent_shadow_comparison_verdicts(
|
|
2344
2963
|
from_ts=now - _RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS,
|
|
2345
2964
|
to_ts=now,
|
|
2346
2965
|
judge_prompt_version=pinned_version,
|
|
2966
|
+
limit=clamped_limit,
|
|
2347
2967
|
)
|
|
2348
2968
|
except NotImplementedError:
|
|
2349
2969
|
# Backends that don't support shadow verdicts (e.g., Disk) should
|
|
@@ -2351,9 +2971,7 @@ def get_recent_shadow_comparisons(
|
|
|
2351
2971
|
# "no data yet" in the UI.
|
|
2352
2972
|
return GetRecentShadowComparisonsResponse(verdicts=[])
|
|
2353
2973
|
|
|
2354
|
-
|
|
2355
|
-
newest_first = list(reversed(verdicts))[:clamped_limit]
|
|
2356
|
-
return GetRecentShadowComparisonsResponse(verdicts=newest_first)
|
|
2974
|
+
return GetRecentShadowComparisonsResponse(verdicts=verdicts)
|
|
2357
2975
|
|
|
2358
2976
|
|
|
2359
2977
|
@core_router.post(
|
|
@@ -2709,11 +3327,107 @@ def _add_openapi_security(app: FastAPI) -> None:
|
|
|
2709
3327
|
app.openapi = custom_openapi # type: ignore[method-assign]
|
|
2710
3328
|
|
|
2711
3329
|
|
|
3330
|
+
def _resolve_lifespan_org_id(get_org_id: Callable[..., str] | None) -> str:
|
|
3331
|
+
"""Resolve the bootstrap org ID for lifespan schedulers without a request context.
|
|
3332
|
+
|
|
3333
|
+
Args:
|
|
3334
|
+
get_org_id (Callable[..., str] | None): Custom org-ID dependency, or None for
|
|
3335
|
+
the default single-tenant mode.
|
|
3336
|
+
|
|
3337
|
+
Returns:
|
|
3338
|
+
str: The resolved org ID string.
|
|
3339
|
+
"""
|
|
3340
|
+
from reflexio.server.auth import default_get_org_id
|
|
3341
|
+
|
|
3342
|
+
if get_org_id is None:
|
|
3343
|
+
return default_get_org_id()
|
|
3344
|
+
try:
|
|
3345
|
+
signature = inspect.signature(get_org_id)
|
|
3346
|
+
except (TypeError, ValueError):
|
|
3347
|
+
return default_get_org_id()
|
|
3348
|
+
if signature.parameters:
|
|
3349
|
+
return default_get_org_id()
|
|
3350
|
+
try:
|
|
3351
|
+
return str(get_org_id())
|
|
3352
|
+
except Exception:
|
|
3353
|
+
logger.exception("Failed to resolve lifespan org_id; using default org")
|
|
3354
|
+
return default_get_org_id()
|
|
3355
|
+
|
|
3356
|
+
|
|
3357
|
+
def _wire_capabilities(
|
|
3358
|
+
app: FastAPI,
|
|
3359
|
+
capabilities: "CapabilityRegistry | None",
|
|
3360
|
+
mount_data_plane: bool,
|
|
3361
|
+
additional_routers: list[APIRouter] | None = None,
|
|
3362
|
+
) -> None:
|
|
3363
|
+
"""Wire capability routers, services, and hooks into the app at construction time.
|
|
3364
|
+
|
|
3365
|
+
No-op when ``capabilities`` is None.
|
|
3366
|
+
|
|
3367
|
+
The deployment role passed to each capability's ``routers(role)`` is
|
|
3368
|
+
sourced from ``capabilities.role`` when set (threaded in by the enterprise
|
|
3369
|
+
composition root via ``build_registry(deployment_role())``). When
|
|
3370
|
+
``capabilities.role`` is ``None`` the role falls back to the
|
|
3371
|
+
``mount_data_plane`` derivation (``"all"`` when True, ``"control-plane"``
|
|
3372
|
+
when False), preserving the existing OSS-only behaviour.
|
|
3373
|
+
|
|
3374
|
+
Args:
|
|
3375
|
+
app (FastAPI): The application instance to wire into.
|
|
3376
|
+
capabilities (CapabilityRegistry | None): Capabilities to install, or None.
|
|
3377
|
+
mount_data_plane (bool): Whether the data-plane role is active.
|
|
3378
|
+
additional_routers (list[APIRouter] | None): Routers already mounted via
|
|
3379
|
+
``additional_routers``; used to detect and reject double-mounts at boot.
|
|
3380
|
+
|
|
3381
|
+
Raises:
|
|
3382
|
+
ValueError: If a capability's router object is also present in
|
|
3383
|
+
``additional_routers`` — each router must be mounted exactly once.
|
|
3384
|
+
"""
|
|
3385
|
+
if capabilities is None:
|
|
3386
|
+
return
|
|
3387
|
+
from reflexio.server.auth import default_billing_gate
|
|
3388
|
+
from reflexio.server.extensions import HookRegistry
|
|
3389
|
+
|
|
3390
|
+
if capabilities.role is not None:
|
|
3391
|
+
role = capabilities.role
|
|
3392
|
+
else:
|
|
3393
|
+
role = "all" if mount_data_plane else "control-plane"
|
|
3394
|
+
if capabilities.configurator_class is not None:
|
|
3395
|
+
from reflexio.server.services.configurator.configurator import (
|
|
3396
|
+
set_configurator_class,
|
|
3397
|
+
)
|
|
3398
|
+
|
|
3399
|
+
set_configurator_class(capabilities.configurator_class)
|
|
3400
|
+
if capabilities.billing_gate is not None:
|
|
3401
|
+
for line in ("application", "learnings_generated"):
|
|
3402
|
+
app.dependency_overrides[default_billing_gate(line)] = (
|
|
3403
|
+
capabilities.billing_gate(line)
|
|
3404
|
+
)
|
|
3405
|
+
# HookRegistry is a stateless facade: its methods configure process-global
|
|
3406
|
+
# tracer/usage-recorder/retrieval-capture singletons; the object needn't be stored.
|
|
3407
|
+
hooks = HookRegistry()
|
|
3408
|
+
additional = additional_routers or []
|
|
3409
|
+
for cap in capabilities.capabilities:
|
|
3410
|
+
cap.install_services()
|
|
3411
|
+
cap.install_hooks(hooks)
|
|
3412
|
+
for r in cap.routers(role):
|
|
3413
|
+
if any(r is a for a in additional):
|
|
3414
|
+
raise ValueError(
|
|
3415
|
+
f"router for capability {cap.name!r} is mounted both via the "
|
|
3416
|
+
f"registry and additional_routers; mount it exactly once"
|
|
3417
|
+
)
|
|
3418
|
+
app.include_router(r)
|
|
3419
|
+
|
|
3420
|
+
|
|
2712
3421
|
def create_app(
|
|
2713
3422
|
get_org_id: Callable[..., str] | None = None,
|
|
2714
3423
|
additional_routers: list[APIRouter] | None = None,
|
|
2715
3424
|
middleware_config: dict | None = None,
|
|
2716
3425
|
require_auth: bool = False,
|
|
3426
|
+
get_caller_type: Callable[..., str] | None = None,
|
|
3427
|
+
get_billing_gate: Callable[[str], Callable[..., None]] | None = None,
|
|
3428
|
+
mount_data_plane: bool = True,
|
|
3429
|
+
capabilities: "CapabilityRegistry | None" = None,
|
|
3430
|
+
app_context_factory: "Callable[[], AppContext] | None" = None,
|
|
2717
3431
|
) -> FastAPI:
|
|
2718
3432
|
"""Factory to create a FastAPI app.
|
|
2719
3433
|
|
|
@@ -2724,54 +3438,114 @@ def create_app(
|
|
|
2724
3438
|
middleware_config: Optional middleware overrides (not used yet, reserved for future).
|
|
2725
3439
|
require_auth: When True, declares a Bearer security scheme in the OpenAPI spec
|
|
2726
3440
|
so Swagger UI shows lock icons and the Authorize button works.
|
|
3441
|
+
get_caller_type: Custom dependency for classifying the caller (e.g., production
|
|
3442
|
+
agent vs dashboard). When provided, overrides the default_get_caller_type
|
|
3443
|
+
dependency globally, exactly mirroring the get_org_id override.
|
|
3444
|
+
get_billing_gate: Optional factory ``(line: str) -> FastAPI dependency`` that
|
|
3445
|
+
replaces the default no-op gate for each billable billing line. When
|
|
3446
|
+
provided, for every line used in the app (``"application"`` and
|
|
3447
|
+
``"learnings_generated"``) the returned dependency overrides the
|
|
3448
|
+
``default_billing_gate(line)`` sentinel in ``dependency_overrides``,
|
|
3449
|
+
exactly mirroring the ``get_caller_type`` override pattern.
|
|
3450
|
+
mount_data_plane: When True (default), include the data-plane routers
|
|
3451
|
+
(core, stall-state, pending-tool-call) and run the data-plane
|
|
3452
|
+
lifespan work (LLM availability check, cross-encoder prewarm,
|
|
3453
|
+
resume scheduler). When False, skip both so a control-plane host
|
|
3454
|
+
can build an app without requiring LLM/storage or starting the
|
|
3455
|
+
scheduler, while keeping all other scaffolding (middleware, CORS,
|
|
3456
|
+
auth overrides, OpenAPI security, health, ``/meta/version``,
|
|
3457
|
+
``additional_routers``).
|
|
3458
|
+
capabilities: Optional registry of enterprise capabilities. When provided,
|
|
3459
|
+
each capability's routers, services, hooks, and lifecycle methods are
|
|
3460
|
+
wired into the app at construction time. Behavior is unchanged when
|
|
3461
|
+
``None``.
|
|
3462
|
+
app_context_factory: Optional callable returning the AppContext passed to
|
|
3463
|
+
each capability's on_startup. When None, an empty AppContext() is used
|
|
3464
|
+
(local OSS / tests). Enterprise binds this to supply self_host_org_id /
|
|
3465
|
+
activated computed during its own startup.
|
|
2727
3466
|
|
|
2728
3467
|
Returns:
|
|
2729
3468
|
Configured FastAPI application.
|
|
2730
|
-
|
|
3469
|
+
|
|
3470
|
+
Raises:
|
|
3471
|
+
ValueError: If both ``get_billing_gate`` and ``capabilities.billing_gate``
|
|
3472
|
+
are provided — pass billing_gate through exactly one path.
|
|
3473
|
+
"""
|
|
3474
|
+
if (
|
|
3475
|
+
get_billing_gate is not None
|
|
3476
|
+
and capabilities is not None
|
|
3477
|
+
and capabilities.billing_gate is not None
|
|
3478
|
+
):
|
|
3479
|
+
raise ValueError(
|
|
3480
|
+
"pass billing_gate via either get_billing_gate or capabilities, not both"
|
|
3481
|
+
)
|
|
2731
3482
|
from collections.abc import AsyncIterator
|
|
2732
3483
|
from contextlib import asynccontextmanager
|
|
2733
3484
|
|
|
2734
|
-
from reflexio.server._auth import default_get_org_id
|
|
2735
3485
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
3486
|
+
from reflexio.server.auth import (
|
|
3487
|
+
default_billing_gate,
|
|
3488
|
+
default_get_caller_type,
|
|
3489
|
+
default_get_org_id,
|
|
3490
|
+
)
|
|
3491
|
+
from reflexio.server.extensions import AppContext
|
|
2736
3492
|
from reflexio.server.llm.model_defaults import validate_llm_availability
|
|
2737
3493
|
from reflexio.server.services.extraction.resume_scheduler import (
|
|
2738
3494
|
maybe_start_resume_scheduler,
|
|
2739
3495
|
)
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
return default_get_org_id()
|
|
2744
|
-
try:
|
|
2745
|
-
signature = inspect.signature(get_org_id)
|
|
2746
|
-
except (TypeError, ValueError):
|
|
2747
|
-
return default_get_org_id()
|
|
2748
|
-
if signature.parameters:
|
|
2749
|
-
return default_get_org_id()
|
|
2750
|
-
try:
|
|
2751
|
-
return str(get_org_id())
|
|
2752
|
-
except Exception:
|
|
2753
|
-
logger.exception("Failed to resolve lifespan org_id; using default org")
|
|
2754
|
-
return default_get_org_id()
|
|
3496
|
+
from reflexio.server.services.lineage.gc_scheduler import (
|
|
3497
|
+
maybe_start_lineage_gc,
|
|
3498
|
+
)
|
|
2755
3499
|
|
|
2756
3500
|
@asynccontextmanager
|
|
2757
3501
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3502
|
+
scheduler = None
|
|
3503
|
+
gc_scheduler = None
|
|
3504
|
+
started_caps: list = []
|
|
3505
|
+
if mount_data_plane:
|
|
3506
|
+
log_publish_hardware_capacity()
|
|
3507
|
+
validate_llm_availability()
|
|
3508
|
+
from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
|
|
3509
|
+
|
|
3510
|
+
_prewarm_cross_encoder()
|
|
3511
|
+
# The scheduler discovers every org with resumable work each tick and
|
|
3512
|
+
# drives a per-org worker with org-scoped claims, so it is not limited
|
|
3513
|
+
# to the bootstrap org. The bootstrap org is only used to read config
|
|
3514
|
+
# and to seed cross-org discovery.
|
|
3515
|
+
bootstrap_org_id = _resolve_lifespan_org_id(get_org_id)
|
|
3516
|
+
scheduler = maybe_start_resume_scheduler(
|
|
3517
|
+
lambda org_id: RequestContext(org_id=org_id),
|
|
3518
|
+
bootstrap_org_id=bootstrap_org_id,
|
|
3519
|
+
)
|
|
3520
|
+
gc_scheduler = maybe_start_lineage_gc(
|
|
3521
|
+
lambda org_id: RequestContext(org_id=org_id),
|
|
3522
|
+
bootstrap_org_id=bootstrap_org_id,
|
|
3523
|
+
)
|
|
2770
3524
|
try:
|
|
3525
|
+
if capabilities is not None:
|
|
3526
|
+
ctx = (
|
|
3527
|
+
app_context_factory()
|
|
3528
|
+
if app_context_factory is not None
|
|
3529
|
+
else AppContext()
|
|
3530
|
+
)
|
|
3531
|
+
for cap in capabilities.capabilities:
|
|
3532
|
+
await cap.on_startup(ctx)
|
|
3533
|
+
started_caps.append(cap)
|
|
2771
3534
|
yield
|
|
2772
3535
|
finally:
|
|
3536
|
+
for cap in reversed(started_caps):
|
|
3537
|
+
try:
|
|
3538
|
+
await cap.on_shutdown()
|
|
3539
|
+
except Exception:
|
|
3540
|
+
logger.warning(
|
|
3541
|
+
"capability %r on_shutdown raised; continuing cleanup",
|
|
3542
|
+
cap,
|
|
3543
|
+
exc_info=True,
|
|
3544
|
+
)
|
|
2773
3545
|
if scheduler is not None:
|
|
2774
3546
|
scheduler.stop()
|
|
3547
|
+
if gc_scheduler is not None:
|
|
3548
|
+
gc_scheduler.stop()
|
|
2775
3549
|
|
|
2776
3550
|
app = FastAPI(docs_url="/docs", lifespan=lifespan)
|
|
2777
3551
|
|
|
@@ -2837,6 +3611,21 @@ def create_app(
|
|
|
2837
3611
|
if get_org_id is not None:
|
|
2838
3612
|
app.dependency_overrides[default_get_org_id] = get_org_id
|
|
2839
3613
|
|
|
3614
|
+
# Override get_caller_type dependency if custom one provided
|
|
3615
|
+
if get_caller_type is not None:
|
|
3616
|
+
app.dependency_overrides[default_get_caller_type] = get_caller_type
|
|
3617
|
+
|
|
3618
|
+
# Override billing gate dependencies if a custom gate factory is provided.
|
|
3619
|
+
# Each billing line needs its own override because dependency_overrides is
|
|
3620
|
+
# keyed by callable identity. ``default_billing_gate`` uses lru_cache so
|
|
3621
|
+
# the same sentinel object is returned for the same line — which is why
|
|
3622
|
+
# the overrides reliably fire at request time.
|
|
3623
|
+
if get_billing_gate is not None:
|
|
3624
|
+
for line in ("application", "learnings_generated"):
|
|
3625
|
+
app.dependency_overrides[default_billing_gate(line)] = get_billing_gate(
|
|
3626
|
+
line
|
|
3627
|
+
)
|
|
3628
|
+
|
|
2840
3629
|
# When a custom get_org_id is provided together with require_auth,
|
|
2841
3630
|
# auth is enforced on every route — mark this app instance so the
|
|
2842
3631
|
# token-gated my_config endpoint becomes reachable. Using
|
|
@@ -2845,19 +3634,26 @@ def create_app(
|
|
|
2845
3634
|
# multi-tenant embeddings) can coexist without leaking state.
|
|
2846
3635
|
app.state.my_config_enabled = bool(get_org_id is not None and require_auth)
|
|
2847
3636
|
|
|
2848
|
-
# Include core
|
|
2849
|
-
|
|
3637
|
+
# Include data-plane routes (core, stall-state, pending-tool-call). A
|
|
3638
|
+
# control-plane host sets mount_data_plane=False to skip these while
|
|
3639
|
+
# keeping every other piece of scaffolding below.
|
|
3640
|
+
if mount_data_plane:
|
|
3641
|
+
# Include core routes
|
|
3642
|
+
app.include_router(core_router)
|
|
2850
3643
|
|
|
2851
|
-
|
|
2852
|
-
|
|
3644
|
+
# Include stall_state routes
|
|
3645
|
+
app.include_router(stall_state_api.router)
|
|
2853
3646
|
|
|
2854
|
-
|
|
2855
|
-
|
|
3647
|
+
# Include pending tool call routes
|
|
3648
|
+
app.include_router(pending_tool_call_api.router)
|
|
2856
3649
|
|
|
2857
3650
|
# Include additional routers
|
|
2858
3651
|
for router in additional_routers or []:
|
|
2859
3652
|
app.include_router(router)
|
|
2860
3653
|
|
|
3654
|
+
# Wire capability routers, services, and hooks (composition-root only)
|
|
3655
|
+
_wire_capabilities(app, capabilities, mount_data_plane, additional_routers)
|
|
3656
|
+
|
|
2861
3657
|
# Health/observability endpoint (per-worker metrics for recycling)
|
|
2862
3658
|
health_api.install(app)
|
|
2863
3659
|
|