mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-23 05:54:22 +00:00
refactor(plugin): split agent-runner action handlers out of handler.py
Extract the AgentRunner Protocol v1 host-side surface from the giant RuntimeConnectionHandler.__init__ into sibling modules using a registration- function pattern (behavior-preserving; @h.action == @self.action): - agent_run_support.py: shared constants + authorization/scope/projection helpers - agent_pull_actions.py: register(h) for history/event pull APIs - agent_runner_actions.py: register(h) for run/runtime/stats/claim lifecycle - agent_state_actions.py: register(h) for steering/state APIs __init__ now calls the three register(self) functions. handler.py keeps the pre-existing plugin/llm/vector/knowledge handlers, get_prompt/call_tool/ get_tool_detail (coupled to retained helpers), shared helpers, and outbound methods; it re-imports _validate_agent_run_session so external imports keep working. handler.py: 4066 -> 1871 lines. test_state_api_auth.py: repoint get_session_registry patch targets to agent_run_support (the lookup moved modules). 385 agent unit tests pass; ruff clean.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
"""Agent-runner pull actions (history / event)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
from langbot_plugin.runtime.io import handler
|
||||
from langbot_plugin.entities.io.actions.enums import (
|
||||
PluginToRuntimeAction,
|
||||
)
|
||||
|
||||
|
||||
from .agent_run_support import (
|
||||
_get_run_authorization,
|
||||
_validate_agent_run_session,
|
||||
_resolve_run_conversation,
|
||||
_run_scope_filters,
|
||||
_event_matches_run_scope,
|
||||
_project_event_record_for_api,
|
||||
)
|
||||
|
||||
|
||||
def register(h):
|
||||
@h.action(PluginToRuntimeAction.HISTORY_PAGE)
|
||||
async def history_page(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Page through transcript history for a conversation.
|
||||
|
||||
Requires run_id authorization. Only allows access to current run's conversation.
|
||||
"""
|
||||
run_id = data.get('run_id')
|
||||
conversation_id = data.get('conversation_id')
|
||||
before_cursor = data.get('before_cursor')
|
||||
after_cursor = data.get('after_cursor')
|
||||
limit = data.get('limit', 50)
|
||||
direction = data.get('direction', 'backward')
|
||||
include_attachments = data.get('include_attachments', False)
|
||||
caller_plugin_identity = data.get('caller_plugin_identity')
|
||||
|
||||
if not run_id:
|
||||
return handler.ActionResponse.error(message='run_id is required')
|
||||
|
||||
session, error = await _validate_agent_run_session(
|
||||
run_id,
|
||||
caller_plugin_identity,
|
||||
h.ap,
|
||||
'History page',
|
||||
api_capability='history_page',
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
|
||||
conversation_id, scope_error = _resolve_run_conversation(
|
||||
session,
|
||||
conversation_id,
|
||||
'History page',
|
||||
)
|
||||
if scope_error:
|
||||
return scope_error
|
||||
|
||||
if not conversation_id:
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'items': [],
|
||||
'next_cursor': None,
|
||||
'prev_cursor': None,
|
||||
'has_more': False,
|
||||
}
|
||||
)
|
||||
|
||||
# Parse cursors
|
||||
before_seq = int(before_cursor) if before_cursor else None
|
||||
after_seq = int(after_cursor) if after_cursor else None
|
||||
|
||||
# Query transcript
|
||||
from ..agent.runner.transcript_store import TranscriptStore
|
||||
|
||||
store = TranscriptStore(h.ap.persistence_mgr.get_db_engine())
|
||||
|
||||
try:
|
||||
items, next_seq, prev_seq, has_more = await store.page_transcript(
|
||||
conversation_id=conversation_id,
|
||||
before_seq=before_seq,
|
||||
after_seq=after_seq,
|
||||
limit=limit,
|
||||
direction=direction,
|
||||
include_attachments=include_attachments,
|
||||
**_run_scope_filters(session),
|
||||
)
|
||||
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'items': items,
|
||||
'next_cursor': str(next_seq) if next_seq else None,
|
||||
'prev_cursor': str(prev_seq) if prev_seq else None,
|
||||
'has_more': has_more,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
h.ap.logger.error(f'HISTORY_PAGE error: {e}', exc_info=True)
|
||||
return handler.ActionResponse.error(message=f'History page error: {e}')
|
||||
|
||||
@h.action(PluginToRuntimeAction.HISTORY_SEARCH)
|
||||
async def history_search(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Search transcript history.
|
||||
|
||||
Requires run_id authorization. Only searches current run's conversation.
|
||||
Basic implementation using LIKE filtering.
|
||||
"""
|
||||
run_id = data.get('run_id')
|
||||
query_text = data.get('query', '')
|
||||
filters = data.get('filters') or {}
|
||||
top_k = data.get('top_k', 10)
|
||||
caller_plugin_identity = data.get('caller_plugin_identity')
|
||||
|
||||
if not run_id:
|
||||
return handler.ActionResponse.error(message='run_id is required')
|
||||
|
||||
session, error = await _validate_agent_run_session(
|
||||
run_id,
|
||||
caller_plugin_identity,
|
||||
h.ap,
|
||||
'History search',
|
||||
api_capability='history_search',
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
|
||||
requested_conversation_id = filters.get('conversation_id')
|
||||
conversation_id, scope_error = _resolve_run_conversation(
|
||||
session,
|
||||
requested_conversation_id,
|
||||
'History search',
|
||||
)
|
||||
if scope_error:
|
||||
return scope_error
|
||||
|
||||
if not conversation_id:
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'items': [],
|
||||
'total_count': 0,
|
||||
'query': query_text,
|
||||
}
|
||||
)
|
||||
|
||||
# Search transcript
|
||||
from ..agent.runner.transcript_store import TranscriptStore
|
||||
|
||||
store = TranscriptStore(h.ap.persistence_mgr.get_db_engine())
|
||||
|
||||
try:
|
||||
safe_filters = {k: v for k, v in filters.items() if k != 'conversation_id'}
|
||||
items = await store.search_transcript(
|
||||
conversation_id=conversation_id,
|
||||
query_text=query_text,
|
||||
filters=safe_filters,
|
||||
top_k=top_k,
|
||||
**_run_scope_filters(session),
|
||||
)
|
||||
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'items': items,
|
||||
'total_count': len(items),
|
||||
'query': query_text,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
h.ap.logger.error(f'HISTORY_SEARCH error: {e}', exc_info=True)
|
||||
return handler.ActionResponse.error(message=f'History search error: {e}')
|
||||
|
||||
@h.action(PluginToRuntimeAction.EVENT_GET)
|
||||
async def event_get(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Get a single event record by ID.
|
||||
|
||||
Requires run_id authorization. Only allows access to events in current run's conversation.
|
||||
"""
|
||||
run_id = data.get('run_id')
|
||||
event_id = data.get('event_id')
|
||||
caller_plugin_identity = data.get('caller_plugin_identity')
|
||||
|
||||
if not run_id:
|
||||
return handler.ActionResponse.error(message='run_id is required')
|
||||
|
||||
if not event_id:
|
||||
return handler.ActionResponse.error(message='event_id is required')
|
||||
|
||||
session, error = await _validate_agent_run_session(
|
||||
run_id,
|
||||
caller_plugin_identity,
|
||||
h.ap,
|
||||
'Event get',
|
||||
api_capability='event_get',
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
|
||||
# Get event
|
||||
from ..agent.runner.event_log_store import EventLogStore
|
||||
|
||||
store = EventLogStore(h.ap.persistence_mgr.get_db_engine())
|
||||
|
||||
try:
|
||||
event = await store.get_event(event_id)
|
||||
if not event:
|
||||
return handler.ActionResponse.error(message=f'Event {event_id} not found')
|
||||
|
||||
# Validate event is in the same conversation as the run, or was created by the same run.
|
||||
session_conversation_id = _get_run_authorization(session).get('conversation_id')
|
||||
event_run_id = event.get('run_id')
|
||||
if event_run_id and event_run_id == run_id:
|
||||
return handler.ActionResponse.success(data=_project_event_record_for_api(event))
|
||||
if not session_conversation_id or not _event_matches_run_scope(session, event):
|
||||
return handler.ActionResponse.error(message=f'Event {event_id} is not accessible by this run')
|
||||
|
||||
return handler.ActionResponse.success(data=_project_event_record_for_api(event))
|
||||
except Exception as e:
|
||||
h.ap.logger.error(f'EVENT_GET error: {e}', exc_info=True)
|
||||
return handler.ActionResponse.error(message=f'Event get error: {e}')
|
||||
|
||||
@h.action(PluginToRuntimeAction.EVENT_PAGE)
|
||||
async def event_page(data: dict[str, Any]) -> handler.ActionResponse:
|
||||
"""Page through event records.
|
||||
|
||||
Requires run_id authorization. Only allows access to current run's conversation.
|
||||
"""
|
||||
run_id = data.get('run_id')
|
||||
conversation_id = data.get('conversation_id')
|
||||
event_types = data.get('event_types')
|
||||
before_cursor = data.get('before_cursor')
|
||||
limit = data.get('limit', 50)
|
||||
caller_plugin_identity = data.get('caller_plugin_identity')
|
||||
|
||||
if not run_id:
|
||||
return handler.ActionResponse.error(message='run_id is required')
|
||||
|
||||
session, error = await _validate_agent_run_session(
|
||||
run_id,
|
||||
caller_plugin_identity,
|
||||
h.ap,
|
||||
'Event page',
|
||||
api_capability='event_page',
|
||||
)
|
||||
if error:
|
||||
return error
|
||||
|
||||
conversation_id, scope_error = _resolve_run_conversation(
|
||||
session,
|
||||
conversation_id,
|
||||
'Event page',
|
||||
)
|
||||
if scope_error:
|
||||
return scope_error
|
||||
|
||||
if not conversation_id:
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'items': [],
|
||||
'next_cursor': None,
|
||||
'prev_cursor': None,
|
||||
'has_more': False,
|
||||
}
|
||||
)
|
||||
|
||||
# Parse cursor
|
||||
before_seq = int(before_cursor) if before_cursor else None
|
||||
|
||||
# Query events
|
||||
from ..agent.runner.event_log_store import EventLogStore
|
||||
|
||||
store = EventLogStore(h.ap.persistence_mgr.get_db_engine())
|
||||
|
||||
try:
|
||||
items, next_seq, has_more = await store.page_events(
|
||||
conversation_id=conversation_id,
|
||||
event_types=event_types,
|
||||
before_seq=before_seq,
|
||||
limit=limit,
|
||||
**_run_scope_filters(session),
|
||||
)
|
||||
|
||||
return handler.ActionResponse.success(
|
||||
data={
|
||||
'items': [_project_event_record_for_api(item) for item in items],
|
||||
'next_cursor': str(next_seq) if next_seq else None,
|
||||
'prev_cursor': None,
|
||||
'has_more': has_more,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
h.ap.logger.error(f'EVENT_PAGE error: {e}', exc_info=True)
|
||||
return handler.ActionResponse.error(message=f'Event page error: {e}')
|
||||
Reference in New Issue
Block a user