claude-smart 0.2.46 → 0.2.48
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 +1 -1
- package/README.md +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +11 -6
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- 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/gc_scheduler.py +385 -78
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- 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 +955 -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 +844 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -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/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
|
@@ -195,12 +195,12 @@ _PROVIDER_DEFAULTS: dict[str, ProviderDefaults] = {
|
|
|
195
195
|
extraction_agent="gpt-5.5",
|
|
196
196
|
),
|
|
197
197
|
"anthropic": ProviderDefaults(
|
|
198
|
-
generation="claude-sonnet-
|
|
199
|
-
evaluation="claude-sonnet-
|
|
198
|
+
generation="claude-sonnet-5",
|
|
199
|
+
evaluation="claude-sonnet-5",
|
|
200
200
|
should_run="claude-haiku-4-5-20251001",
|
|
201
201
|
pre_retrieval="claude-haiku-4-5-20251001",
|
|
202
202
|
embedding=None,
|
|
203
|
-
extraction_agent="claude-sonnet-
|
|
203
|
+
extraction_agent="claude-sonnet-5",
|
|
204
204
|
),
|
|
205
205
|
"gemini": ProviderDefaults(
|
|
206
206
|
generation="gemini/gemini-3-flash-preview",
|
|
@@ -229,7 +229,7 @@ _PROVIDER_DEFAULTS: dict[str, ProviderDefaults] = {
|
|
|
229
229
|
should_run="minimax/MiniMax-M3",
|
|
230
230
|
pre_retrieval="minimax/MiniMax-M3",
|
|
231
231
|
embedding=None,
|
|
232
|
-
# Same
|
|
232
|
+
# Same M3 model handles resumable extraction. Surfaced by an
|
|
233
233
|
# e2e run on a MiniMax-only VPS where publish printed
|
|
234
234
|
# "No provider in ['minimax'] supports role=extraction_agent"
|
|
235
235
|
# warnings and silently skipped profile creation. Without this,
|
|
@@ -62,14 +62,22 @@ _ENV_TIMEOUT = "CLAUDE_SMART_CLI_TIMEOUT"
|
|
|
62
62
|
_ENV_MODEL = "CLAUDE_SMART_CLI_MODEL"
|
|
63
63
|
_HOST_CODEX = "codex"
|
|
64
64
|
_HOST_CLAUDE_CODE = "claude-code"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_windows() -> bool:
|
|
68
|
+
return os.name == "nt"
|
|
69
|
+
|
|
70
|
+
|
|
65
71
|
_CODEX_COMPAT_SCRIPT_NAMES = (
|
|
66
72
|
("codex-claude-compat.cmd", "codex-claude-compat")
|
|
67
|
-
if
|
|
73
|
+
if _is_windows()
|
|
68
74
|
else ("codex-claude-compat", "codex-claude-compat.cmd")
|
|
69
75
|
)
|
|
70
76
|
_CODEX_COMPAT_SCRIPT_NAME_SET = set(_CODEX_COMPAT_SCRIPT_NAMES)
|
|
71
77
|
_DEFAULT_TIMEOUT_SECONDS = 120
|
|
72
|
-
_DEFAULT_CLI_MODEL = "claude-sonnet-
|
|
78
|
+
_DEFAULT_CLI_MODEL = "claude-sonnet-5"
|
|
79
|
+
_WINDOWS_ARGV_SYSTEM_PROMPT_LIMIT = 3_000
|
|
80
|
+
_WINDOWS_CLI_SUFFIXES = (".cmd", ".exe", ".bat")
|
|
73
81
|
|
|
74
82
|
_TRUTHY_ENV_VALUES = {"1", "true", "yes"}
|
|
75
83
|
_UNSUPPORTED_PARAMS_WARNED: set[str] = set()
|
|
@@ -116,6 +124,40 @@ def _candidate_codex_compat_path() -> Path | None:
|
|
|
116
124
|
return None
|
|
117
125
|
|
|
118
126
|
|
|
127
|
+
def _windows_cli_suffixes() -> tuple[str, ...]:
|
|
128
|
+
suffixes: list[str] = []
|
|
129
|
+
for suffix in os.environ.get("PATHEXT", "").split(";"):
|
|
130
|
+
suffix = suffix.strip().lower()
|
|
131
|
+
if not suffix:
|
|
132
|
+
continue
|
|
133
|
+
suffix = suffix if suffix.startswith(".") else f".{suffix}"
|
|
134
|
+
# PowerShell scripts require a PowerShell host; do not return them as
|
|
135
|
+
# directly executable CLI shims for subprocess.run([...]).
|
|
136
|
+
if suffix == ".ps1":
|
|
137
|
+
continue
|
|
138
|
+
suffixes.append(suffix)
|
|
139
|
+
for suffix in _WINDOWS_CLI_SUFFIXES:
|
|
140
|
+
if suffix not in suffixes:
|
|
141
|
+
suffixes.append(suffix)
|
|
142
|
+
return tuple(suffixes)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _resolve_cli_override_path(cli_path: str) -> str:
|
|
146
|
+
if not _is_windows():
|
|
147
|
+
return cli_path
|
|
148
|
+
path = Path(cli_path)
|
|
149
|
+
if path.suffix:
|
|
150
|
+
return cli_path
|
|
151
|
+
|
|
152
|
+
# Windows package managers expose extensioned shims while users often
|
|
153
|
+
# configure the extensionless binary name.
|
|
154
|
+
for suffix in _windows_cli_suffixes():
|
|
155
|
+
candidate = path.with_suffix(suffix)
|
|
156
|
+
if candidate.exists():
|
|
157
|
+
return str(candidate)
|
|
158
|
+
return cli_path
|
|
159
|
+
|
|
160
|
+
|
|
119
161
|
def _resolve_cli_path() -> str | None:
|
|
120
162
|
"""Return the path to the active host CLI, or None if unavailable.
|
|
121
163
|
|
|
@@ -129,7 +171,7 @@ def _resolve_cli_path() -> str | None:
|
|
|
129
171
|
"""
|
|
130
172
|
override = os.environ.get(_ENV_CLI_PATH)
|
|
131
173
|
if override:
|
|
132
|
-
candidate = Path(override)
|
|
174
|
+
candidate = Path(_resolve_cli_override_path(override))
|
|
133
175
|
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
134
176
|
return str(candidate)
|
|
135
177
|
_LOGGER.warning(
|
|
@@ -432,6 +474,9 @@ def _run_claude_stream(
|
|
|
432
474
|
) -> ParseResult:
|
|
433
475
|
"""Invoke ``claude -p --output-format stream-json`` and return a ParseResult."""
|
|
434
476
|
model = os.environ.get(_ENV_MODEL) or _DEFAULT_CLI_MODEL
|
|
477
|
+
inline_system_prompt = (
|
|
478
|
+
_is_windows() and len(system_prompt) > _WINDOWS_ARGV_SYSTEM_PROMPT_LIMIT
|
|
479
|
+
)
|
|
435
480
|
cmd = [
|
|
436
481
|
cli_path,
|
|
437
482
|
"-p",
|
|
@@ -442,8 +487,11 @@ def _run_claude_stream(
|
|
|
442
487
|
"--model",
|
|
443
488
|
model,
|
|
444
489
|
]
|
|
445
|
-
if system_prompt:
|
|
490
|
+
if system_prompt and not inline_system_prompt:
|
|
446
491
|
cmd.extend(["--append-system-prompt", system_prompt])
|
|
492
|
+
stdin_text = dialogue
|
|
493
|
+
if inline_system_prompt:
|
|
494
|
+
stdin_text = f"{system_prompt}\n\n{dialogue}" if dialogue else system_prompt
|
|
447
495
|
|
|
448
496
|
# Tag the child process so any hooks it fires (e.g. claude-smart's
|
|
449
497
|
# Stop hook) can detect that this is a reflexio-internal invocation
|
|
@@ -460,9 +508,11 @@ def _run_claude_stream(
|
|
|
460
508
|
try:
|
|
461
509
|
proc = subprocess.run( # noqa: S603 — cmd is constructed from validated parts.
|
|
462
510
|
cmd,
|
|
463
|
-
input=
|
|
511
|
+
input=stdin_text,
|
|
464
512
|
capture_output=True,
|
|
465
513
|
text=True,
|
|
514
|
+
encoding="utf-8",
|
|
515
|
+
errors="replace" if _is_windows() else "strict",
|
|
466
516
|
timeout=timeout_seconds,
|
|
467
517
|
check=False,
|
|
468
518
|
env=env,
|
|
@@ -511,6 +561,8 @@ def _run_codex_stream(
|
|
|
511
561
|
input=_codex_prompt(prompt=dialogue, system_prompt=system_prompt),
|
|
512
562
|
capture_output=True,
|
|
513
563
|
text=True,
|
|
564
|
+
encoding="utf-8",
|
|
565
|
+
errors="replace" if _is_windows() else "strict",
|
|
514
566
|
timeout=timeout_seconds,
|
|
515
567
|
check=False,
|
|
516
568
|
env=env,
|
|
@@ -150,7 +150,18 @@ def score_pairs(query: str, docs: list[str]) -> list[float]:
|
|
|
150
150
|
return []
|
|
151
151
|
model = _get_model()
|
|
152
152
|
pairs = [(query, doc) for doc in docs]
|
|
153
|
-
|
|
153
|
+
try:
|
|
154
|
+
from torch import nn
|
|
155
|
+
except ImportError as e:
|
|
156
|
+
raise CrossEncoderUnavailableError(
|
|
157
|
+
"torch is not installed; cannot use the cross-encoder reranker"
|
|
158
|
+
) from e
|
|
159
|
+
|
|
160
|
+
raw_scores = model.predict(
|
|
161
|
+
pairs,
|
|
162
|
+
show_progress_bar=False,
|
|
163
|
+
activation_fn=nn.Identity(),
|
|
164
|
+
)
|
|
154
165
|
# ``predict`` returns a numpy array; convert to plain Python floats so
|
|
155
166
|
# the caller can serialise the result without numpy as a dependency.
|
|
156
167
|
return [float(s) for s in raw_scores]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""HTTP middleware for the OSS FastAPI app (Tier3 A2 extraction from api.py).
|
|
2
|
+
|
|
3
|
+
Holds the five middleware classes ``create_app`` wires in (in that order) plus
|
|
4
|
+
the request-limit / CORS-origin helpers and their tunable constants. No
|
|
5
|
+
middleware reads mutable module-global app state — every read is of an
|
|
6
|
+
import-time constant or an env var — so this extraction is behavior-preserving.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from anyio.to_thread import current_default_thread_limiter
|
|
14
|
+
from fastapi import Request, status
|
|
15
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
16
|
+
from starlette.responses import Response
|
|
17
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
18
|
+
|
|
19
|
+
from reflexio.server.correlation import correlation_id_var, generate_correlation_id
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Bot protection configuration
|
|
24
|
+
REQUEST_TIMEOUT_SECONDS = 60
|
|
25
|
+
SYNC_REQUEST_TIMEOUT_SECONDS = (
|
|
26
|
+
600 # Longer timeout for synchronous processing (wait_for_response=true)
|
|
27
|
+
)
|
|
28
|
+
SUSPICIOUS_USER_AGENTS = ["bot", "crawler", "spider", "scraper", "curl", "wget"]
|
|
29
|
+
ALLOWED_EMPTY_UA_PATHS = ["/health", "/"] # Paths that allow empty user agents
|
|
30
|
+
DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_cors_origins() -> list[str]:
|
|
34
|
+
"""Resolve browser origins allowed to make credentialed CORS requests."""
|
|
35
|
+
configured_origins = os.getenv("REFLEXIO_ALLOWED_ORIGINS", "").strip()
|
|
36
|
+
if configured_origins:
|
|
37
|
+
origins = [
|
|
38
|
+
origin.strip().rstrip("/")
|
|
39
|
+
for origin in configured_origins.split(",")
|
|
40
|
+
if origin.strip()
|
|
41
|
+
]
|
|
42
|
+
return origins or ["http://localhost:8080"]
|
|
43
|
+
|
|
44
|
+
frontend_url = os.getenv("FRONTEND_URL", "").strip()
|
|
45
|
+
if frontend_url:
|
|
46
|
+
return [frontend_url.rstrip("/")]
|
|
47
|
+
|
|
48
|
+
return ["http://localhost:8080"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _max_body_bytes_from_env() -> int:
|
|
52
|
+
raw_value = os.getenv("REFLEXIO_MAX_BODY_BYTES", str(DEFAULT_MAX_BODY_BYTES))
|
|
53
|
+
try:
|
|
54
|
+
max_bytes = int(raw_value)
|
|
55
|
+
except ValueError:
|
|
56
|
+
logger.warning(
|
|
57
|
+
"Ignoring invalid REFLEXIO_MAX_BODY_BYTES=%r; using %s",
|
|
58
|
+
raw_value,
|
|
59
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
60
|
+
)
|
|
61
|
+
return DEFAULT_MAX_BODY_BYTES
|
|
62
|
+
if max_bytes <= 0:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Ignoring non-positive REFLEXIO_MAX_BODY_BYTES=%r; using %s",
|
|
65
|
+
raw_value,
|
|
66
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
67
|
+
)
|
|
68
|
+
return DEFAULT_MAX_BODY_BYTES
|
|
69
|
+
return max_bytes
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BotProtectionMiddleware(BaseHTTPMiddleware):
|
|
73
|
+
"""Middleware to detect and block suspicious bot-like requests."""
|
|
74
|
+
|
|
75
|
+
async def dispatch(
|
|
76
|
+
self, request: Request, call_next: RequestResponseEndpoint
|
|
77
|
+
) -> Response:
|
|
78
|
+
"""Process request and block suspicious patterns.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
request (Request): The incoming request
|
|
82
|
+
call_next (RequestResponseEndpoint): Next middleware/handler in chain
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Response: The response from the next handler or a 403 JSON response
|
|
86
|
+
"""
|
|
87
|
+
from starlette.responses import JSONResponse
|
|
88
|
+
|
|
89
|
+
user_agent = request.headers.get("user-agent", "").lower()
|
|
90
|
+
path = request.url.path
|
|
91
|
+
|
|
92
|
+
# Allow health check and root without user agent
|
|
93
|
+
if path not in ALLOWED_EMPTY_UA_PATHS:
|
|
94
|
+
# Block requests with no user agent
|
|
95
|
+
if not user_agent:
|
|
96
|
+
return JSONResponse(
|
|
97
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
98
|
+
content={"detail": "Forbidden: Missing user agent"},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Block requests with suspicious user agents
|
|
102
|
+
for suspicious in SUSPICIOUS_USER_AGENTS:
|
|
103
|
+
if suspicious in user_agent:
|
|
104
|
+
return JSONResponse(
|
|
105
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
106
|
+
content={"detail": "Forbidden: Suspicious user agent"},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return await call_next(request)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TimeoutMiddleware(BaseHTTPMiddleware):
|
|
113
|
+
"""Middleware to enforce request timeout."""
|
|
114
|
+
|
|
115
|
+
async def dispatch(
|
|
116
|
+
self, request: Request, call_next: RequestResponseEndpoint
|
|
117
|
+
) -> Response:
|
|
118
|
+
"""Process request with timeout enforcement.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
request (Request): The incoming request
|
|
122
|
+
call_next (RequestResponseEndpoint): Next middleware/handler in chain
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Response: The response from the next handler or a 504 JSON response
|
|
126
|
+
"""
|
|
127
|
+
from starlette.responses import JSONResponse
|
|
128
|
+
|
|
129
|
+
# Use longer timeout for synchronous processing requests
|
|
130
|
+
timeout = REQUEST_TIMEOUT_SECONDS
|
|
131
|
+
if request.query_params.get("wait_for_response", "").lower() == "true":
|
|
132
|
+
timeout = SYNC_REQUEST_TIMEOUT_SECONDS
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
return await asyncio.wait_for(call_next(request), timeout=timeout)
|
|
136
|
+
except TimeoutError:
|
|
137
|
+
return JSONResponse(
|
|
138
|
+
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
|
139
|
+
content={"detail": "Request timeout"},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _RequestBodyTooLargeError(Exception):
|
|
144
|
+
"""Raised when the streamed request body exceeds the configured limit."""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class BodySizeLimitMiddleware:
|
|
148
|
+
"""Reject requests whose declared or streamed body size exceeds the limit."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
151
|
+
self.app = app
|
|
152
|
+
|
|
153
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
154
|
+
from starlette.responses import JSONResponse
|
|
155
|
+
|
|
156
|
+
if scope["type"] != "http":
|
|
157
|
+
await self.app(scope, receive, send)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
max_body_bytes = _max_body_bytes_from_env()
|
|
161
|
+
content_length = None
|
|
162
|
+
for name, value in scope.get("headers", []):
|
|
163
|
+
if name.lower() == b"content-length":
|
|
164
|
+
content_length = value.decode("latin-1")
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
if content_length is not None:
|
|
168
|
+
try:
|
|
169
|
+
body_bytes = int(content_length)
|
|
170
|
+
except ValueError:
|
|
171
|
+
body_bytes = 0
|
|
172
|
+
if body_bytes > max_body_bytes:
|
|
173
|
+
await JSONResponse(
|
|
174
|
+
status_code=status.HTTP_413_CONTENT_TOO_LARGE,
|
|
175
|
+
content={"detail": "Request body too large"},
|
|
176
|
+
)(scope, receive, send)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
consumed_bytes = 0
|
|
180
|
+
|
|
181
|
+
async def limited_receive() -> Message:
|
|
182
|
+
nonlocal consumed_bytes
|
|
183
|
+
message = await receive()
|
|
184
|
+
if message["type"] == "http.request":
|
|
185
|
+
consumed_bytes += len(message.get("body", b""))
|
|
186
|
+
if consumed_bytes > max_body_bytes:
|
|
187
|
+
raise _RequestBodyTooLargeError
|
|
188
|
+
return message
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
await self.app(scope, limited_receive, send)
|
|
192
|
+
except _RequestBodyTooLargeError:
|
|
193
|
+
await JSONResponse(
|
|
194
|
+
status_code=status.HTTP_413_CONTENT_TOO_LARGE,
|
|
195
|
+
content={"detail": "Request body too large"},
|
|
196
|
+
)(scope, receive, send)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
200
|
+
"""Attach conservative browser security headers to every response."""
|
|
201
|
+
|
|
202
|
+
async def dispatch(
|
|
203
|
+
self, request: Request, call_next: RequestResponseEndpoint
|
|
204
|
+
) -> Response:
|
|
205
|
+
response = await call_next(request)
|
|
206
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
207
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
208
|
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
209
|
+
if (
|
|
210
|
+
request.url.scheme == "https"
|
|
211
|
+
or request.headers.get("x-forwarded-proto", "").lower() == "https"
|
|
212
|
+
):
|
|
213
|
+
response.headers["Strict-Transport-Security"] = (
|
|
214
|
+
"max-age=31536000; includeSubDomains"
|
|
215
|
+
)
|
|
216
|
+
return response
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
220
|
+
"""Middleware that assigns a unique correlation ID to each request.
|
|
221
|
+
|
|
222
|
+
The ID is stored in a ContextVar so it propagates to log records
|
|
223
|
+
(via CorrelationIdFilter) and to ThreadPoolExecutor workers when
|
|
224
|
+
``contextvars.copy_context()`` is used.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
async def dispatch(
|
|
228
|
+
self, request: Request, call_next: RequestResponseEndpoint
|
|
229
|
+
) -> Response:
|
|
230
|
+
cid = generate_correlation_id()
|
|
231
|
+
correlation_id_var.set(cid)
|
|
232
|
+
try:
|
|
233
|
+
stats = current_default_thread_limiter().statistics()
|
|
234
|
+
request.state.tp_borrowed = stats.borrowed_tokens
|
|
235
|
+
request.state.tp_total = stats.total_tokens
|
|
236
|
+
request.state.tp_waiting = stats.tasks_waiting
|
|
237
|
+
except Exception as exc: # noqa: BLE001
|
|
238
|
+
logger.debug("Failed to snapshot threadpool limiter stats: %s", exc)
|
|
239
|
+
request.state.tp_borrowed = None
|
|
240
|
+
request.state.tp_total = None
|
|
241
|
+
request.state.tp_waiting = None
|
|
242
|
+
response = await call_next(request)
|
|
243
|
+
response.headers["X-Correlation-ID"] = cid
|
|
244
|
+
return response
|
|
@@ -62,6 +62,11 @@ def _limit_for(operation: OperationName) -> int:
|
|
|
62
62
|
return _DEFAULT_LIMITS[operation]
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def operation_limit_value(operation: OperationName) -> int:
|
|
66
|
+
"""Return the configured concurrency limit for an operation."""
|
|
67
|
+
return _limit_for(operation)
|
|
68
|
+
|
|
69
|
+
|
|
65
70
|
def _timeout_for(operation: OperationName) -> float:
|
|
66
71
|
raw = os.getenv(_env_key(operation, "TIMEOUT_SECONDS"), "").strip()
|
|
67
72
|
if raw:
|
|
@@ -403,6 +408,7 @@ def operation_limit(
|
|
|
403
408
|
*,
|
|
404
409
|
timeout_seconds: float | None = None,
|
|
405
410
|
wait_forever: bool = False,
|
|
411
|
+
log_timeout: bool = True,
|
|
406
412
|
) -> Any:
|
|
407
413
|
"""Acquire the per-org limiter for an operation, recording wait metrics."""
|
|
408
414
|
state = _state_for(org_id, operation)
|
|
@@ -448,7 +454,7 @@ def operation_limit(
|
|
|
448
454
|
limit=state.limit,
|
|
449
455
|
active=active,
|
|
450
456
|
)
|
|
451
|
-
if operation == "publish":
|
|
457
|
+
if operation == "publish" and log_timeout:
|
|
452
458
|
logger.warning(
|
|
453
459
|
"publish_limiter_timeout org_id=%s limit=%s active=%s "
|
|
454
460
|
"waiting=%s wait_ms=%s timeout_seconds=%s hardware=%s",
|
|
@@ -504,12 +510,14 @@ def run_with_operation_limit[T](
|
|
|
504
510
|
fn: Callable[[], T],
|
|
505
511
|
timeout_seconds: float | None = None,
|
|
506
512
|
wait_forever: bool = False,
|
|
513
|
+
log_timeout: bool = True,
|
|
507
514
|
) -> T:
|
|
508
515
|
with operation_limit(
|
|
509
516
|
org_id,
|
|
510
517
|
operation,
|
|
511
518
|
timeout_seconds=timeout_seconds,
|
|
512
519
|
wait_forever=wait_forever,
|
|
520
|
+
log_timeout=log_timeout,
|
|
513
521
|
):
|
|
514
522
|
return fn()
|
|
515
523
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
active: true
|
|
3
3
|
description: "Context setting prompt for resumable playbook extraction. Every claim must be grounded in the conversation and generalized to a reusable task context; both requirements apply jointly to Correction SOPs and Success Path Recipes."
|
|
4
|
-
changelog: "v4.4.0: deliver the result as a native structured response — output the {{\"playbooks\": [...]}} JSON directly as the final assistant message instead of a finish_extraction tool call; ask_human/attach_pending_info_request remain real intermediate tools, and a plain (no-tool) final turn carrying the JSON is now the valid completion. No output schema change. v4.3.0: expands Correction SOP signals to include avoidable follow-ups, partial delivery, stalled conditional authorization, unverified claims, and incomplete coverage; requires Correction SOPs whenever correction signals are present; adds antecedent-not-reaction trigger guidance. (in-place 2026-06-18: make triggers future-user-query-like retrieval keys.) (in-place 2026-06-15: adds concise retrieve-general/action-specific guidance.) v4.2.3: strengthens strict tool-call discipline so plain-text/no-tool responses are explicitly invalid, including empty extraction outcomes. v4.2.2: replaces output examples with compact tool-call and JSON-shape guidance, while preserving the resumable extraction contract. v4.2.1: negative/avoid guidance cannot contradict the final verified implementation or final evaluation. v4.2.0: adds emergent skill-convention guidance for structuring multi-aspect playbook content as a small set of grouped do/avoid rules (no schema change). v4.1.7: adds resumable extraction guidance, names legacy fallback and malformed-input branches in boundary-change recipes, and avoids a polarity output field. (in-place 2026-05-30: tool-oriented finish_extraction output framing; deprecated and removed blocking_issue.) (in-place 2026-05-30: clarify in the Output Format section that finish_extraction may be preceded by ask_human/attach_pending_info_request; restructure examples into one no-tool example plus a worked ask_human and attach_pending_info_request example; stress that ask_human is rare and reserved for critical missing org-level facts — finish_extraction alone is the norm.) (in-place 2026-05-30: condense the resumable guidance — state finish-required/optional once, replace the duplicated Output Format block with a one-line pointer, and trim example prose.) (in-place 2026-06-03: consolidate and shorten the ask_human condition: ask only for missing shared context needed to know what to do, while still finishing extraction.) (in-place 2026-06-03: frame finish_extraction as the final completion tool and pending-info tools as intermediate.) (in-place 2026-06-03: clarify that empty finish_extraction alone must not replace ask_human when the ask_human condition applies.) (in-place 2026-06-04: clarify ask_human and attach_pending_info_request are alternatives for the same gap, not a sequence.)"
|
|
4
|
+
changelog: "v4.4.0: deliver the result as a native structured response — output the {{\"playbooks\": [...]}} JSON directly as the final assistant message instead of a finish_extraction tool call; ask_human/attach_pending_info_request remain real intermediate tools, and a plain (no-tool) final turn carrying the JSON is now the valid completion. No output schema change. (in-place 2026-06-28: strengthen the ask_human decision boundary for cases where a correction proves a durable shared procedure exists but only avoidance is grounded.) v4.3.0: expands Correction SOP signals to include avoidable follow-ups, partial delivery, stalled conditional authorization, unverified claims, and incomplete coverage; requires Correction SOPs whenever correction signals are present; adds antecedent-not-reaction trigger guidance. (in-place 2026-06-18: make triggers future-user-query-like retrieval keys.) (in-place 2026-06-15: adds concise retrieve-general/action-specific guidance.) v4.2.3: strengthens strict tool-call discipline so plain-text/no-tool responses are explicitly invalid, including empty extraction outcomes. v4.2.2: replaces output examples with compact tool-call and JSON-shape guidance, while preserving the resumable extraction contract. v4.2.1: negative/avoid guidance cannot contradict the final verified implementation or final evaluation. v4.2.0: adds emergent skill-convention guidance for structuring multi-aspect playbook content as a small set of grouped do/avoid rules (no schema change). v4.1.7: adds resumable extraction guidance, names legacy fallback and malformed-input branches in boundary-change recipes, and avoids a polarity output field. (in-place 2026-05-30: tool-oriented finish_extraction output framing; deprecated and removed blocking_issue.) (in-place 2026-05-30: clarify in the Output Format section that finish_extraction may be preceded by ask_human/attach_pending_info_request; restructure examples into one no-tool example plus a worked ask_human and attach_pending_info_request example; stress that ask_human is rare and reserved for critical missing org-level facts — finish_extraction alone is the norm.) (in-place 2026-05-30: condense the resumable guidance — state finish-required/optional once, replace the duplicated Output Format block with a one-line pointer, and trim example prose.) (in-place 2026-06-03: consolidate and shorten the ask_human condition: ask only for missing shared context needed to know what to do, while still finishing extraction.) (in-place 2026-06-03: frame finish_extraction as the final completion tool and pending-info tools as intermediate.) (in-place 2026-06-03: clarify that empty finish_extraction alone must not replace ask_human when the ask_human condition applies.) (in-place 2026-06-04: clarify ask_human and attach_pending_info_request are alternatives for the same gap, not a sequence.)"
|
|
5
5
|
variables:
|
|
6
6
|
- agent_context_prompt
|
|
7
7
|
- extraction_definition_prompt
|
|
@@ -26,6 +26,16 @@ Choose exactly one intermediate path per missing fact: use `attach_pending_info_
|
|
|
26
26
|
|
|
27
27
|
Do not ask for user-scoped/private facts, context derivable from the trajectory, agent context, tool list, or Prior Knowledge, complete avoidance-only rules, or merely nice-to-have details.
|
|
28
28
|
|
|
29
|
+
### Ask-human decision boundary
|
|
30
|
+
|
|
31
|
+
Before finalizing any Correction SOP from a negative correction, run this check:
|
|
32
|
+
|
|
33
|
+
1. Did the correction merely establish a complete avoidance rule, with no evidence that a shared positive procedure exists? If yes, emit the grounded avoidance rule and do not ask.
|
|
34
|
+
2. Did the correction state or strongly imply that a shared policy, process, approval path, escalation route, canonical target, standard wording, or other durable positive procedure exists, while leaving the actual target/procedure unknown? If yes, the future playbook is incomplete: call `ask_human` (or `attach_pending_info_request` if Prior Knowledge already has the same pending question).
|
|
35
|
+
3. Did the missing detail concern a single user, customer, client, assignment, record, or current factual lookup rather than a reusable agent/org rule? If yes, do not ask; emit only grounded guidance or no playbook.
|
|
36
|
+
|
|
37
|
+
Phrases like "specific process", "required workflow", "approval path", "special route", "canonical path/target", "standard policy", "standard wording/script", or "do not know the exact name/text" are strong evidence of a shared positive procedure gap when the conversation does not provide the missing procedure. Avoidance-only playbooks are acceptable interim guidance, but they are not a substitute for `ask_human` when the conversation proves that future agents need a missing shared positive action to handle the class of task correctly. The question should ask for the smallest durable missing fact, such as the approved route, required procedure, canonical target, policy threshold, or standard wording.
|
|
38
|
+
|
|
29
39
|
If an intermediate tool is needed, call exactly one of the two intermediate tools before delivering your final result. Then finalize the current run with any independently valid playbooks now, including avoidance rules; if none are valid without the answer, finalize with `{{"playbooks": []}}`.
|
|
30
40
|
|
|
31
41
|
When the `ask_human` condition applies, an `ask_human` tool call is required before finalization. Do not replace the question with an empty `{{"playbooks": []}}` final response alone.
|
|
@@ -161,6 +171,8 @@ Grounding constrains the *source* of a claim (it must come from the conversation
|
|
|
161
171
|
|
|
162
172
|
**When the agent doesn't know what to do:** Describe what to AVOID (the observed mistake) and state the limitation honestly. It is much better to say "do not fabricate order status — admit you cannot look it up" than to invent a specific alternative the agent may not actually have. If the missing "what to do" detail satisfies the `ask_human` condition above, use the tool; otherwise emit only the grounded avoidance rule or no playbook.
|
|
163
173
|
|
|
174
|
+
If a user correction says there is a specific process, approval path, route, standard, or policy but does not provide it, the extractor does not know enough to produce the durable positive SOP. Do not silently downgrade that case to avoidance-only unless avoidance is the complete reusable lesson; ask for the missing shared procedure and still finalize any grounded interim playbooks.
|
|
175
|
+
|
|
164
176
|
**Rule of thumb (both checks must pass):**
|
|
165
177
|
1. *Grounded* — if you remove the conversation and only read the `content`, could someone verify every claim by re-reading the conversation? If not, you've hallucinated.
|
|
166
178
|
2. *Generalized* — could a future agent in a similar task context, with no access to this conversation, apply the entry as written? If not, you've overfit — restate the pattern at the level of situation, artifact role, action sequence, verification, and detour.
|
|
@@ -175,7 +187,7 @@ For **Correction SOPs**:
|
|
|
175
187
|
3. Identify the violated implicit expectation
|
|
176
188
|
4. Draft the `trigger` (the problem or situation)
|
|
177
189
|
5. Tautology Check (see above)
|
|
178
|
-
6. Draft `content`: reason through what the agent did wrong and what the user's feedback tells us the agent should do differently. Ground every statement in evidence from the conversation. If the user told the agent what to do, capture that. If the user only told the agent what NOT to do, capture the avoidance. Do not guess what the right action is if the conversation doesn't tell you; use `ask_human` only when the condition above applies.
|
|
190
|
+
6. Draft `content`: reason through what the agent did wrong and what the user's feedback tells us the agent should do differently. Ground every statement in evidence from the conversation. If the user told the agent what to do, capture that. If the user only told the agent what NOT to do, capture the avoidance. If the user indicated that a shared process/policy/route/approval/standard/canonical target exists but did not provide it, call `ask_human` or attach to matching Prior Knowledge before finalizing; the avoidance rule alone does not answer what a future agent should do. Do not guess what the right action is if the conversation doesn't tell you; use `ask_human` only when the condition above applies.
|
|
179
191
|
|
|
180
192
|
For **Success Path Recipes**:
|
|
181
193
|
1. Identify whether the agent completed the task successfully
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Shared slowapi rate limiter for the OSS FastAPI app (Tier3 A2).
|
|
2
|
+
|
|
3
|
+
The ``limiter`` singleton and ``configure_rate_limiter`` live here so the domain
|
|
4
|
+
route modules can decorate handlers with ``@limiter.limit(...)`` without importing
|
|
5
|
+
from ``reflexio.server.api`` (which would be circular — ``api`` aggregates the
|
|
6
|
+
route modules). ``reflexio.server.api`` re-exports ``limiter`` and
|
|
7
|
+
``configure_rate_limiter`` so the existing enterprise imports keep resolving.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fastapi import Request
|
|
14
|
+
from slowapi import Limiter
|
|
15
|
+
from slowapi.util import get_remote_address
|
|
16
|
+
|
|
17
|
+
from reflexio.server.tracing import profile_step
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_rate_limit_key(request: Request) -> str:
|
|
21
|
+
"""Get rate limit key based on IP address.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
request (Request): The incoming request
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: Rate limit key (IP address)
|
|
28
|
+
"""
|
|
29
|
+
return get_remote_address(request)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _storage_backend_name(limiter_obj: Limiter) -> str:
|
|
33
|
+
storage = getattr(limiter_obj, "_storage", None)
|
|
34
|
+
if storage is None:
|
|
35
|
+
return "unknown"
|
|
36
|
+
return storage.__class__.__name__
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _trace_external_rate_limit_backend(limiter_obj: Limiter) -> None:
|
|
40
|
+
"""Trace rate-limit storage hits when the backend is an external service."""
|
|
41
|
+
backend = _storage_backend_name(limiter_obj)
|
|
42
|
+
if backend == "MemoryStorage":
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
strategy = getattr(limiter_obj, "limiter", None)
|
|
46
|
+
if strategy is None or getattr(strategy, "_reflexio_traced", False):
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
original_hit = strategy.hit
|
|
50
|
+
|
|
51
|
+
def traced_hit(item: Any, *identifiers: str, cost: int = 1) -> bool:
|
|
52
|
+
with profile_step(
|
|
53
|
+
"rate_limit.backend_hit",
|
|
54
|
+
storage_backend=backend,
|
|
55
|
+
strategy=strategy.__class__.__name__,
|
|
56
|
+
cost=cost,
|
|
57
|
+
):
|
|
58
|
+
return original_hit(item, *identifiers, cost=cost)
|
|
59
|
+
|
|
60
|
+
strategy.hit = traced_hit
|
|
61
|
+
strategy._reflexio_traced = True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Initialize rate limiter
|
|
65
|
+
limiter = Limiter(key_func=get_rate_limit_key)
|
|
66
|
+
_trace_external_rate_limit_backend(limiter)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def configure_rate_limiter(key_func: Callable[..., str]) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Replace the rate limiter's key function.
|
|
72
|
+
|
|
73
|
+
This is the supported way to override the default IP-based key function
|
|
74
|
+
(e.g. with an org-scoped or token-scoped variant in the enterprise layer).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
key_func: A callable that accepts a Request and returns a string key.
|
|
78
|
+
"""
|
|
79
|
+
limiter._key_func = key_func # type: ignore[reportAttributeAccessIssue]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Domain route modules for the OSS FastAPI app (Tier3 A2 split of api.py).
|
|
2
|
+
|
|
3
|
+
Each module owns its own ``APIRouter`` and is aggregated into
|
|
4
|
+
``reflexio.server.api.core_router`` so the single ``core_router`` remains the
|
|
5
|
+
data-plane router surface the enterprise composition mounts and iterates.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared route helpers used across domain route modules (Tier3 A2)."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from reflexio.server.operation_limiter import (
|
|
6
|
+
OperationName,
|
|
7
|
+
limiter_http_exception,
|
|
8
|
+
run_with_operation_limit,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run_limited_api[T](
|
|
13
|
+
org_id: str,
|
|
14
|
+
operation: OperationName,
|
|
15
|
+
fn: Callable[[], T],
|
|
16
|
+
) -> T:
|
|
17
|
+
try:
|
|
18
|
+
return run_with_operation_limit(
|
|
19
|
+
org_id=org_id,
|
|
20
|
+
operation=operation,
|
|
21
|
+
fn=fn,
|
|
22
|
+
)
|
|
23
|
+
except TimeoutError as exc:
|
|
24
|
+
http_exc = limiter_http_exception(operation)
|
|
25
|
+
raise http_exc from exc
|