mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
- skills/ (builtin SKILL.md assets) has zero Python importers; it is read by filesystem path only. Moved the dir and restored skills_backends._default_builtin_root() to the clean parent.parent / "skills" / "builtin" form (undoing the transient path from 5c). - plugin_loader.py -> shared (frozen chat_deepagent uses it -> re-export shim). - plugins/ package -> shared (year_substituter rewired to shared.plugin_loader; docstring entry-point example updated to the shared dotted path). No shim needed (only a test imported it). Plugin discovery is via importlib entry points (group "surfsense.plugins"), not dotted-path import, and nothing is registered in pyproject, so the move does not affect runtime discovery.
88 lines
3.1 KiB
Python
88 lines
3.1 KiB
Python
"""Reference plugin: substitute ``{{year}}`` in tool descriptions.
|
|
|
|
Demonstrates the :meth:`AgentMiddleware.awrap_tool_call` hook -- the
|
|
plugin sees every tool invocation and can rewrite the request *or* the
|
|
result. This particular plugin is read-only and only transforms the
|
|
*description* the user might see in error messages (no request
|
|
mutation).
|
|
|
|
The plugin is built as a factory function so the entry-point loader can
|
|
inject :class:`PluginContext` (containing the agent's LLM, search-space
|
|
ID, etc.). The factory signature
|
|
``Callable[[PluginContext], AgentMiddleware]`` is the only contract --
|
|
SurfSense doesn't define a custom plugin protocol on top of LangChain's
|
|
:class:`AgentMiddleware`.
|
|
|
|
Wire-up in ``pyproject.toml`` (illustrative; the in-repo plugin doesn't
|
|
need this -- it's already on the import path)::
|
|
|
|
[project.entry-points."surfsense.plugins"]
|
|
year_substituter = "app.agents.shared.plugins.year_substituter:make_middleware"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from datetime import UTC, datetime
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from langchain.agents.middleware import AgentMiddleware
|
|
|
|
if TYPE_CHECKING: # pragma: no cover - type-only
|
|
from langchain.agents.middleware.types import ToolCallRequest
|
|
from langchain_core.messages import ToolMessage
|
|
from langgraph.types import Command
|
|
|
|
from app.agents.shared.plugin_loader import PluginContext
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class _YearSubstituterMiddleware(AgentMiddleware):
|
|
"""Replace ``{{year}}`` in the result text with the current UTC year."""
|
|
|
|
tools = ()
|
|
|
|
def __init__(self, year: int | None = None) -> None:
|
|
super().__init__()
|
|
self._year = str(year if year is not None else datetime.now(UTC).year)
|
|
|
|
async def awrap_tool_call(
|
|
self,
|
|
request: ToolCallRequest,
|
|
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
|
|
) -> ToolMessage | Command[Any]:
|
|
result = await handler(request)
|
|
try:
|
|
from langchain_core.messages import ToolMessage
|
|
|
|
if (
|
|
isinstance(result, ToolMessage)
|
|
and isinstance(result.content, str)
|
|
and "{{year}}" in result.content
|
|
):
|
|
new_text = result.content.replace("{{year}}", self._year)
|
|
result = ToolMessage(
|
|
content=new_text,
|
|
tool_call_id=result.tool_call_id,
|
|
id=result.id,
|
|
name=result.name,
|
|
status=result.status,
|
|
artifact=result.artifact,
|
|
)
|
|
except Exception: # pragma: no cover - defensive
|
|
logger.exception("year_substituter plugin failed; passing original result")
|
|
return result
|
|
|
|
|
|
def make_middleware(ctx: PluginContext) -> AgentMiddleware:
|
|
"""Plugin factory used by :func:`load_plugin_middlewares`."""
|
|
# Plugin is intentionally small so it has no state to threading-protect
|
|
# and ignores ``ctx`` beyond demonstrating that the loader passes it in.
|
|
_ = ctx
|
|
return _YearSubstituterMiddleware()
|
|
|
|
|
|
__all__ = ["make_middleware"]
|