chore: import upstream snapshot with attribution
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Explore-context loop capability.
|
||||
|
||||
A near-invisible loop capability that activates whenever the chat turn carries
|
||||
any readable (non-image) attached source — a document, a notebook record, a
|
||||
book section, a question-bank entry, or — the motivating case — a referenced
|
||||
conversation history. When active it runs a read-only pre-pass
|
||||
(:class:`ContextExplorer`) *before* the answer loop's first LLM call: an
|
||||
agentic investigation that uses ``read_source`` to read the attached sources
|
||||
the user's request actually needs, then folds an objective, third-person
|
||||
investigation into the loop's user-message seed.
|
||||
|
||||
Why it exists:
|
||||
|
||||
* The chat loop fuses "understand the attached material" with "answer the
|
||||
user" in a single loop. When the material is a transcript of the user
|
||||
talking to another AI agent, the model reads those ``## Assistant`` turns in
|
||||
the same context it answers from and adopts that agent's first-person voice.
|
||||
Separating comprehension into an objective pre-pass removes that confusion
|
||||
structurally.
|
||||
* Weak models under native tool calling routinely never call ``read_source``
|
||||
themselves. Owning source-reading in a dedicated pre-pass — and dropping the
|
||||
tool from the answer loop entirely — forces the investigation to happen up
|
||||
front instead of being skipped.
|
||||
|
||||
The capability owns no answer-loop tools and contributes no system block — it
|
||||
works purely through the optional async ``pre_loop`` hook (see
|
||||
:class:`LoopCapability`). ``read_source`` lives inside the pre-pass's own tool
|
||||
loop, not on the answer loop's surface.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import resources
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from deeptutor.capabilities.protocol import PromptBlock
|
||||
from deeptutor.core.context import UnifiedContext
|
||||
from deeptutor.core.stream_bus import StreamBus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROMPT_CACHE: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _load_prompts(language: str) -> dict[str, Any]:
|
||||
lang = "zh" if str(language or "en").lower().startswith("zh") else "en"
|
||||
cached = _PROMPT_CACHE.get(lang)
|
||||
if cached is not None:
|
||||
return cached
|
||||
try:
|
||||
text = (
|
||||
resources.files(__package__)
|
||||
.joinpath("prompts", lang, "explore_context.yaml")
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
data = yaml.safe_load(text)
|
||||
except Exception:
|
||||
logger.warning("failed to load explore_context prompts (%s)", lang, exc_info=True)
|
||||
data = None
|
||||
result = data if isinstance(data, dict) else {}
|
||||
_PROMPT_CACHE[lang] = result
|
||||
return result
|
||||
|
||||
|
||||
def _has_readable_sources(context: UnifiedContext) -> bool:
|
||||
"""Whether the turn has any readable (non-image) attached source.
|
||||
|
||||
``source_index`` is the per-turn ``{source_id: full_text}`` map the chat
|
||||
pipeline builds from the (session-cumulative) Attached Sources manifest. It
|
||||
is non-empty whenever the turn carries any textual source — whether
|
||||
attached this turn or carried over from an earlier turn on the branch — so
|
||||
the investigation runs query-driven on every turn that has sources to read,
|
||||
not just the turn they were first attached.
|
||||
"""
|
||||
idx = context.metadata.get("source_index")
|
||||
return isinstance(idx, dict) and bool(idx)
|
||||
|
||||
|
||||
class ExploreContextCapability:
|
||||
"""Pre-pass capability that investigates the turn's attached context."""
|
||||
|
||||
name = "explore_context"
|
||||
# Owns no answer-loop tools: ``read_source`` is mounted inside the pre-pass's
|
||||
# own tool loop (:class:`ContextExplorer`), never on the answer surface.
|
||||
owned_tools: tuple[str, ...] = ()
|
||||
|
||||
def is_active(self, context: UnifiedContext) -> bool:
|
||||
return _has_readable_sources(context)
|
||||
|
||||
def system_block(
|
||||
self,
|
||||
context: UnifiedContext,
|
||||
*,
|
||||
language: str,
|
||||
prompts: dict[str, Any],
|
||||
) -> PromptBlock | None:
|
||||
# The investigation is delivered via ``pre_loop`` (user-message seed),
|
||||
# not as a static system block.
|
||||
_ = (context, language, prompts)
|
||||
return None
|
||||
|
||||
def augment_kwargs(
|
||||
self,
|
||||
tool_name: str,
|
||||
kwargs: dict[str, Any],
|
||||
context: UnifiedContext,
|
||||
) -> dict[str, Any]:
|
||||
_ = (tool_name, context)
|
||||
return kwargs
|
||||
|
||||
def pre_loop_seed(self, context: UnifiedContext) -> str:
|
||||
_ = context
|
||||
return ""
|
||||
|
||||
async def pre_loop(
|
||||
self,
|
||||
context: UnifiedContext,
|
||||
stream: StreamBus,
|
||||
*,
|
||||
usage: Any | None = None,
|
||||
) -> PromptBlock | None:
|
||||
if not self.is_active(context):
|
||||
return None
|
||||
# Imported lazily: ``explorer`` pulls in ``services.llm`` /
|
||||
# ``core.agentic``, and this capability is constructed at
|
||||
# ``capabilities`` package-import time — importing it eagerly would form
|
||||
# a circular import through the LLM config stack. By ``pre_loop`` call
|
||||
# time everything is initialised.
|
||||
from deeptutor.capabilities.explore_context.explorer import ContextExplorer
|
||||
|
||||
explorer = ContextExplorer(
|
||||
language=context.language,
|
||||
prompts=_load_prompts(context.language),
|
||||
)
|
||||
investigation = await explorer.investigate(context=context, stream=stream, usage=usage)
|
||||
if not investigation.strip():
|
||||
return None
|
||||
return PromptBlock("explore_context", investigation)
|
||||
|
||||
|
||||
__all__ = ["ExploreContextCapability"]
|
||||
Reference in New Issue
Block a user