agentscope-ai / agentscope-ai/agentscope

Feature request: extensible agent assembly hook in ChatService

Offen
#1,736 3 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
stale-issue
Vorherrschende Sprache
Python
Sterne
31.5k
Forks
3.5k
Ø Merge
1 T. 23 Std.
Gemergte PRs (30 T.)
95

Beschreibung

## Problem

Currently, `ChatService.stream_chat` calls the module-level `get_agent` function directly:

```python
# agentscope/app/_service/_chat.py
from ._agent import get_agent

class ChatService:
async def stream_chat(self, ...):
agent = await get_agent(
storage=self._storage,
workspace_manager=self._workspace_manager,
...
)
```

Because `get_agent` is a free function (not a method on `ChatService`), there is no clean extension point for downstream applications that need to:

1. **Inject additional tools** after agent assembly (e.g. tools that require `model` or `session_id` context not available at the `Workspace.list_tools()` level)
2. **Attach middlewares** to the assembled agent (e.g. dynamic system prompt injection based on per-session state)
3. **Replace `ChatService` entirely** with a subclass to handle frontend interaction differently, without duplicating the full `stream_chat` implementation

## Current workaround

The only working approach is monkey-patching the module-level reference:

```python
import agentscope.app._service._chat as _chat_module

async def _patched_get_agent(storage, workspace_manager, user_id, agent_id, session_id, middlewares=None):
agent = await _orig(storage, workspace_manager, user_id, agent_id, session_id, middlewares)
# inject tools, middlewares, MCPs here
return agent

_chat_module.get_agent = _patched_get_agent
```

This works but is fragile — it depends on the internal module structure and breaks IDE navigation, type checking, and testability.

## Proposed solutions

Any of the following would resolve the issue:

### Option A: Extract `_build_agent` as an overridable method on `ChatService`

```python
class ChatService:
async def _build_agent(self, user_id, agent_id, session_id, middlewares):
return await get_agent(
storage=self._storage,
workspace_manager=self._workspace_manager,
user_id=user_id,
agent_id=agent_id,
session_id=session_id,
middlewares=middlewares,
)

async def stream_chat(self, ...):
agent = await self._build_agent(user_id, agent_id, session_id, middlewares)
```

Subclasses can then override `_build_agent` cleanly.

### Option B: Injectable `ChatService` factory in the chat router

```python
# Allow apps to register a custom ChatService class
app.state.chat_service_class = VulnDigChatService
```

### Option C: `on_agent_created` callback on `WorkspaceManagerBase`

```python
class WorkspaceManagerBase:
async def on_agent_created(self, agent: Agent, user_id: str, session_id: str) -> Agent:
return agent # default: no-op
```

Called by `get_agent` after assembly, giving workspace managers a post-assembly hook.

---

Option A is the smallest change and requires no new abstractions — it simply makes the existing `get_agent` call overridable by subclasses.

## Context

This came up while building a production application on top of agentscope v2 that needs per-session tool injection (workdir-scoped bash, memory tools, spawn-agent delegation). The `Workspace.list_tools()` hook handles most cases but cannot inject tools that depend on the assembled agent's `model` instance.

Beitragsleitfaden

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.