claude-smart 0.2.43 → 0.2.45

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.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +2 -2
  4. package/package.json +2 -2
  5. package/plugin/.claude-plugin/plugin.json +2 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +21 -1
  8. package/plugin/pyproject.toml +3 -3
  9. package/plugin/scripts/_lib.sh +72 -28
  10. package/plugin/scripts/backend-service.sh +55 -7
  11. package/plugin/scripts/cli.sh +2 -1
  12. package/plugin/scripts/dashboard-service.sh +7 -5
  13. package/plugin/scripts/ensure-plugin-root.sh +1 -0
  14. package/plugin/scripts/hook_entry.sh +7 -5
  15. package/plugin/scripts/smart-install.sh +2 -2
  16. package/plugin/src/README.md +57 -0
  17. package/plugin/uv.lock +126 -5
  18. package/plugin/vendor/reflexio/.env.example +9 -0
  19. package/plugin/vendor/reflexio/pyproject.toml +5 -2
  20. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +0 -1
  21. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +7 -4
  22. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +4 -4
  23. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +2 -2
  24. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +13 -4
  25. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +3 -1
  26. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +59 -6
  27. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  28. package/plugin/vendor/reflexio/reflexio/server/README.md +7 -1
  29. package/plugin/vendor/reflexio/reflexio/server/api.py +188 -34
  30. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
  31. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +33 -11
  32. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +278 -29
  33. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +457 -181
  34. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +28 -0
  35. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +22 -12
  36. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +137 -9
  37. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +1 -1
  38. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +36 -3
  39. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +13 -3
  40. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +17 -0
  41. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +1 -1
  42. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
  43. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
  44. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +27 -23
  45. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
  46. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
  47. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +19 -9
  48. package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
  49. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +1 -5
  50. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +44 -2
  51. package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
  52. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +8 -1
  53. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +91 -24
  54. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +3 -1
  55. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +6 -3
  56. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +1 -1
  57. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +18 -5
  58. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +2 -2
  59. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +88 -3
  60. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +36 -5
  61. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +6 -3
  62. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +6 -3
  63. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +32 -22
  64. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +89 -4
  65. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +46 -1
  66. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +12 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +1 -1
  68. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +8 -4
@@ -5,11 +5,14 @@ Utils for service layer
5
5
  import ast
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import re
9
10
  from dataclasses import dataclass
10
11
  from datetime import UTC, datetime
11
12
  from typing import Any
12
13
 
14
+ import tiktoken
15
+
13
16
  from reflexio.cli.log_format import LLM_IO_LOG_FILE, next_llm_entry_id
14
17
  from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
15
18
  from reflexio.models.api_schema.service_schemas import (
@@ -25,6 +28,88 @@ logger = logging.getLogger(__name__)
25
28
  # Already registered in server/__init__.py; import the numeric constant only.
26
29
  MODEL_RESPONSE_LEVEL = 25
27
30
 
31
+ # Per-interaction content token budget when formatting interactions into an
32
+ # extraction/evaluation prompt. Read from REFLEXIO_MAX_INTERACTION_CONTENT_TOKENS;
33
+ # a value <= 0 disables slicing. See _resolve_max_interaction_content_tokens.
34
+ DEFAULT_MAX_INTERACTION_CONTENT_TOKENS = 512
35
+ _MAX_INTERACTION_CONTENT_TOKENS_ENV = "REFLEXIO_MAX_INTERACTION_CONTENT_TOKENS"
36
+ # Inserted between the head and tail of a sliced interaction so the LLM can see
37
+ # that the middle of the content was elided.
38
+ _CONTENT_TRUNCATION_MARKER = " …[truncated] "
39
+ # Encoder used purely as a length heuristic for slicing. tiktoken does not know
40
+ # the real tokenizer for non-OpenAI providers, but cl100k_base is a consistent,
41
+ # good-enough proxy for budgeting prompt content (mirrors the embedding path).
42
+ _CONTENT_ENCODING_NAME = "cl100k_base"
43
+ _content_token_encoding: tiktoken.Encoding | None = None
44
+ # Guards against logging the same malformed env value on every call.
45
+ _INVALID_MAX_TOKENS_WARNED = False
46
+
47
+
48
+ def _get_content_token_encoding() -> tiktoken.Encoding:
49
+ """Return a process-cached tiktoken encoder for content slicing."""
50
+ global _content_token_encoding
51
+ if _content_token_encoding is None:
52
+ _content_token_encoding = tiktoken.get_encoding(_CONTENT_ENCODING_NAME)
53
+ return _content_token_encoding
54
+
55
+
56
+ def _resolve_max_interaction_content_tokens() -> int | None:
57
+ """Resolve the per-interaction content token budget from the environment.
58
+
59
+ Returns:
60
+ int | None: The positive token budget, or None when slicing is
61
+ disabled (env value <= 0). Unset or malformed values fall back to
62
+ ``DEFAULT_MAX_INTERACTION_CONTENT_TOKENS``.
63
+ """
64
+ global _INVALID_MAX_TOKENS_WARNED
65
+ raw = os.getenv(_MAX_INTERACTION_CONTENT_TOKENS_ENV, "").strip()
66
+ if not raw:
67
+ return DEFAULT_MAX_INTERACTION_CONTENT_TOKENS
68
+ try:
69
+ value = int(raw)
70
+ except ValueError:
71
+ if not _INVALID_MAX_TOKENS_WARNED:
72
+ logger.warning(
73
+ "Invalid %s=%r; falling back to default %d",
74
+ _MAX_INTERACTION_CONTENT_TOKENS_ENV,
75
+ raw,
76
+ DEFAULT_MAX_INTERACTION_CONTENT_TOKENS,
77
+ )
78
+ _INVALID_MAX_TOKENS_WARNED = True
79
+ return DEFAULT_MAX_INTERACTION_CONTENT_TOKENS
80
+ return value if value > 0 else None
81
+
82
+
83
+ def slice_content_by_tokens(content: str, max_tokens: int | None) -> str:
84
+ """Slice interaction content to a token budget, keeping head and tail.
85
+
86
+ When the content exceeds ``max_tokens`` tokens, keep the first ``max_tokens
87
+ // 2`` tokens and the last ``max_tokens - max_tokens // 2`` tokens, joined by
88
+ a truncation marker so the elision is visible to the LLM. Content within
89
+ budget (or when ``max_tokens`` is None / content is empty) is returned
90
+ unchanged.
91
+
92
+ Args:
93
+ content (str): The interaction content to slice.
94
+ max_tokens (int | None): Token budget, or None to disable slicing.
95
+
96
+ Returns:
97
+ str: The original or head+marker+tail-sliced content.
98
+ """
99
+ if max_tokens is None or not content:
100
+ return content
101
+ encoding = _get_content_token_encoding()
102
+ tokens = encoding.encode(content)
103
+ if len(tokens) <= max_tokens:
104
+ return content
105
+ head = max_tokens // 2
106
+ tail = max_tokens - head
107
+ return (
108
+ f"{encoding.decode(tokens[:head])}"
109
+ f"{_CONTENT_TRUNCATION_MARKER}"
110
+ f"{encoding.decode(tokens[-tail:])}"
111
+ )
112
+
28
113
 
29
114
  def _format_response_for_logging(response: Any) -> Any:
30
115
  """Render ``ToolCallingChatResponse`` with pretty tool_calls; pass others through.
@@ -188,22 +273,22 @@ def format_interactions_to_history_string(interactions: list[Interaction]) -> st
188
273
  user: I love sushi
189
274
  user: click menu item
190
275
  """
276
+ max_content_tokens = _resolve_max_interaction_content_tokens()
191
277
  formatted_interactions = []
192
278
  for interaction in interactions:
193
279
  # Add text content with tools_used prefix if present
194
280
  if interaction.content:
281
+ content = slice_content_by_tokens(interaction.content, max_content_tokens)
195
282
  if interaction.tools_used:
196
283
  tool_prefix = " ".join(
197
284
  f"[used tool: {t.tool_name}({json.dumps(t.tool_data)})]"
198
285
  for t in interaction.tools_used
199
286
  )
200
287
  formatted_interactions.append(
201
- f"{interaction.role}: ```{tool_prefix} {interaction.content}```"
288
+ f"{interaction.role}: ```{tool_prefix} {content}```"
202
289
  )
203
290
  else:
204
- formatted_interactions.append(
205
- f"{interaction.role}: ```{interaction.content}```"
206
- )
291
+ formatted_interactions.append(f"{interaction.role}: ```{content}```")
207
292
 
208
293
  # Add user action
209
294
  if interaction.user_action != UserActionType.NONE:
@@ -333,6 +333,7 @@ class SQLiteAgentRunMixin:
333
333
  next_resume_at: datetime | None = None,
334
334
  last_error: str | None = None,
335
335
  increment_finalization_attempts: bool = False,
336
+ expected_statuses: tuple[AgentRunStatus, ...] | None = None,
336
337
  ) -> AgentRunRecord | None:
337
338
  current_timestamp = self._current_timestamp()
338
339
  assignments = ["status = ?", "updated_at = ?"]
@@ -361,14 +362,58 @@ class SQLiteAgentRunMixin:
361
362
  assignments.append("finalized_at = ?")
362
363
  params.append(current_timestamp)
363
364
  params.append(run_id)
365
+ status_filter = ""
366
+ if expected_statuses:
367
+ placeholders = ",".join("?" for _ in expected_statuses)
368
+ status_filter = f" AND status IN ({placeholders})"
369
+ params.extend(expected.value for expected in expected_statuses)
364
370
  with self._lock:
365
371
  self.conn.execute(
366
- f"UPDATE _agent_runs SET {', '.join(assignments)} WHERE id = ?",
372
+ f"UPDATE _agent_runs SET {', '.join(assignments)} WHERE id = ?{status_filter}",
367
373
  params,
368
374
  )
369
375
  self.conn.commit()
370
376
  return self.get_agent_run(run_id)
371
377
 
378
+ @SQLiteStorageBase.handle_exceptions
379
+ def fail_running_agent_runs_for_request(
380
+ self,
381
+ *,
382
+ org_id: str,
383
+ extractor_kind: str,
384
+ user_id: str | None,
385
+ request_id: str,
386
+ last_error: str,
387
+ ) -> int:
388
+ current_timestamp = self._current_timestamp()
389
+ with self._lock:
390
+ cursor = self.conn.execute(
391
+ """
392
+ UPDATE _agent_runs
393
+ SET status = ?,
394
+ updated_at = ?,
395
+ last_error = ?
396
+ WHERE org_id = ?
397
+ AND extractor_kind = ?
398
+ AND user_id IS ?
399
+ AND request_id = ?
400
+ AND status IN (?, ?)
401
+ """,
402
+ (
403
+ AgentRunStatus.FAILED.value,
404
+ current_timestamp,
405
+ last_error,
406
+ org_id,
407
+ extractor_kind,
408
+ user_id,
409
+ request_id,
410
+ AgentRunStatus.RUNNING.value,
411
+ AgentRunStatus.RESUMING.value,
412
+ ),
413
+ )
414
+ self.conn.commit()
415
+ return cursor.rowcount
416
+
372
417
  @SQLiteStorageBase.handle_exceptions
373
418
  def create_pending_tool_call(
374
419
  self, record: PendingToolCallRecord
@@ -220,9 +220,21 @@ class AgentRunMixin:
220
220
  next_resume_at: datetime | None = None,
221
221
  last_error: str | None = None,
222
222
  increment_finalization_attempts: bool = False,
223
+ expected_statuses: tuple[AgentRunStatus, ...] | None = None,
223
224
  ) -> AgentRunRecord | None:
224
225
  raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
225
226
 
227
+ def fail_running_agent_runs_for_request(
228
+ self,
229
+ *,
230
+ org_id: str,
231
+ extractor_kind: str,
232
+ user_id: str | None,
233
+ request_id: str,
234
+ last_error: str,
235
+ ) -> int:
236
+ raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
237
+
226
238
  def create_pending_tool_call(
227
239
  self, record: PendingToolCallRecord
228
240
  ) -> PendingToolCallRecord:
@@ -160,7 +160,7 @@ class OperationMixin:
160
160
  payload (dict | None): Optional serialized request payload preserved
161
161
  for the rerun loop. Required so the rerun runs against the SAME
162
162
  interactions the blocked publish enqueued, not whatever the
163
- bookmark currently points at (R2 / reflexio-enterprise#59).
163
+ bookmark currently points at (R2).
164
164
 
165
165
  Returns:
166
166
  dict: Result with keys:
@@ -357,7 +357,8 @@ def _apply_floors(
357
357
  ) -> tuple[list[UserProfile], list[AgentPlaybook], list[UserPlaybook]]:
358
358
  """Apply the per-arm relevance floor to each entity arm in parallel."""
359
359
  with ThreadPoolExecutor(max_workers=3) as ex:
360
- f_profiles = ex.submit(
360
+ f_profiles = _submit_with_current_context(
361
+ ex,
361
362
  apply_relevance_floor,
362
363
  query,
363
364
  profiles,
@@ -365,7 +366,8 @@ def _apply_floors(
365
366
  top_k,
366
367
  arm="profiles",
367
368
  )
368
- f_agent = ex.submit(
369
+ f_agent = _submit_with_current_context(
370
+ ex,
369
371
  apply_relevance_floor,
370
372
  query,
371
373
  agent_playbooks,
@@ -373,7 +375,8 @@ def _apply_floors(
373
375
  top_k,
374
376
  arm="agent_playbooks",
375
377
  )
376
- f_user = ex.submit(
378
+ f_user = _submit_with_current_context(
379
+ ex,
377
380
  apply_relevance_floor,
378
381
  query,
379
382
  user_playbooks,
@@ -533,9 +536,10 @@ def _submit_with_current_context(
533
536
  executor: ThreadPoolExecutor,
534
537
  fn: Callable[..., object],
535
538
  *args: object,
539
+ **kwargs: object,
536
540
  ) -> Future[Any]:
537
541
  context = contextvars.copy_context()
538
- return executor.submit(context.run, fn, *args)
542
+ return executor.submit(context.run, fn, *args, **kwargs)
539
543
 
540
544
 
541
545
  def _storage_backend_name(storage: BaseStorage) -> str: