Files
LangBot/plugins/Runner/dify-agent/README.md

328 lines
13 KiB
Markdown

# Dify Agent
`remove-think` (boolean, default `false`) hides think-tagged reasoning, including unfinished reasoning. `app-type` also accepts `chatflow`.
Run a Dify application as a LangBot Runner.
## Runner ID
`plugin:langbot-team/DifyAgent/default`
## Configuration (Static)
Configuration is **static** and should not contain runtime state. Only the following static fields are supported:
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| base-url | string | yes | https://api.dify.ai/v1 | Dify API base URL |
| advanced-settings | boolean | no | false | Show prompt and timeout tuning controls |
| api-key | secret | yes | '' | Dify Service API key |
| app-type | select | yes | chat | Application type (chat/chatflow/agent/workflow) |
| base-prompt | text | yes | File-handling instruction | Fallback instruction when LangBot input is empty |
| timeout | integer | no | 30 | Request timeout (seconds) |
| langbot-assets-enabled | boolean | no | false | Register a short-lived LangBot asset token for each run and pass it to Dify inputs |
| langbot-assets-gateway-host | string | no | 0.0.0.0 | Host for the local LangBot Asset Gateway |
| langbot-assets-gateway-port | integer | no | 8765 | Port for the local LangBot Asset Gateway |
| langbot-assets-gateway-request-timeout | integer | no | 60 | Timeout for individual gateway tool calls |
| langbot-assets-token-ttl | integer | no | 3600 | Lifetime of each run token in seconds |
| langbot-assets-input-name | string | no | langbot_asset_run_token | Dify input variable that receives the run token |
**Note:** Do not put `conversation_id` in config. Use `RunnerContext.state` for conversation state.
## Capabilities
- `streaming`: yes
- `multimodal_input`: yes
- `interactions`: yes
## Dify 1.18 Agent Applications
Use the Dify Agent Runner with `app-type: agent` and the published application's
Service API URL and key. LocalAgent is a separate, model-backed Runner; a Dify
application's Service API is not an OpenAI-compatible model endpoint.
Dify's new Agent application emits process text as `agent_message` and the
terminal answer as `message`. The Runner treats these as separate streams: once
`message` arrives, its answer replaces the process-text preview instead of being
appended to it. Older agents that emit only `agent_message` continue to work.
Repeated chunks within the same stream are not removed by content equality.
Legacy agents' empty `agent_thought` events do not flush the answer buffer;
`message_end` still closes the response. For workflows, final `summary` outputs
or explicit Answer-node outputs take precedence over buffered `text_chunk`
events, which are used only when no explicit answer was produced.
## Workflow Human Input
When Dify emits `workflow_paused` with a `human_input_required` reason, the
runner asks LangBot to deliver a structured interaction. A validated
`interaction.submitted` event resumes Dify through
`/form/human_input/{form_token}` and `/workflow/{workflow_run_id}/events`.
Workflows that pause again produce a new interaction and can be resumed again.
Forms are emitted as atomic steps: the runner collects one field at a time and
then emits the action buttons. This matches LangBot delivery adapters that
support a single select or an action-only confirmation.
Provider continuation data is not included in the interaction payload. The
runner stores `form_token`, `workflow_run_id`, the Dify user tag, and field/action
mappings in authorized plugin storage under a random LangBot `interaction_id`.
The continuation is deleted after a successful terminal event or after it is
replaced by a later pause.
The selected Runner binding must allow interactions, and the delivery
adapter must support the requested field/action shape. Adapters with a smaller
interaction subset deliver the request's plain-text fallback instead.
The current official adapters expose buttons and single-select callbacks;
free-text and file form fields therefore require a future adapter capability
before they can be resumed from those surfaces.
## Runtime State
Conversation state is managed through `RunnerContext.state` and `RunnerResult.state_updated`:
### Reading State
The runner reads `external.conversation_id` from scoped state:
```python
conversation_id = ctx.state.conversation.get("external.conversation_id")
```
Priority:
1. `ctx.state.conversation["external.conversation_id"]` when it is a Dify UUID
2. Empty string (start a new Dify conversation)
Host-provided LangBot conversation IDs are intentionally not sent to Dify,
because Dify rejects non-UUID `conversation_id` values.
### Updating State
The runner outputs `state.updated` with proper scope:
```python
yield RunnerResult.state_updated(
ctx.run_id,
"external.conversation_id",
dify_conversation_id,
scope="conversation",
)
```
LangBot host persists this state and loads it on the next run.
## LangBot Asset Callback Through Dify MCP
Dify can call back into LangBot assets through the SDK Asset Gateway when the
Dify app has an MCP provider registered against the gateway URL.
### Prerequisites
- The Dify app must be an Agent app with MCP tools enabled.
- The LangBot runner must be installed from this plugin and selected by the
pipeline as `plugin:langbot-team/DifyAgent/default`.
- The SDK Asset Gateway must be reachable by Dify. For Dify Cloud, this means a
public HTTPS URL that forwards to the gateway `/mcp` endpoint. A temporary
tunnel is fine for testing, but use a stable domain or reverse proxy for
production.
- The Dify app needs a Service API key for LangBot to call `https://api.dify.ai/v1`.
For Dify Cloud, the MCP provider URL should look like:
```text
https://example.com/mcp
```
### Configure Dify Cloud
1. Open Dify Cloud and go to **Tools -> MCP**.
2. Create an MCP provider for the LangBot Asset Gateway.
3. Set the provider URL to the public gateway URL, for example
`https://example.com/mcp`.
4. Confirm Dify can discover the LangBot tools. The expected tool set is:
`langbot_list_assets`, `langbot_get_current_event`,
`langbot_history_page`, `langbot_retrieve_knowledge`,
`langbot_get_tool_detail`, and `langbot_call_tool`.
5. Open the target Dify Agent app configuration and attach the LangBot MCP tools.
6. Add a hidden text input variable matching `langbot-assets-input-name`,
defaulting to:
```text
langbot_asset_run_token
```
7. Add prompt guidance telling the app to pass that input value as the
`run_token` argument on every LangBot MCP tool call. A minimal prompt pattern
is:
```text
For every LangBot MCP tool call, set run_token exactly to:
{{langbot_asset_run_token}}
Call langbot_list_assets first when you need to discover available LangBot
assets for the current run.
```
8. Use a tool-compatible chat model. For example, `gpt-4o-mini` works for this
flow. Avoid models that reject Dify's tool result message format; `o3-mini`
has been observed to fail with:
```text
Unsupported value: 'messages[3].role' does not support 'function' with this model.
```
### Configure LangBot
Select the Dify runner on the LangBot pipeline and set:
```text
base-url = https://api.dify.ai/v1
api-key = <Dify app Service API key>
app-type = agent
timeout = 120
langbot-assets-enabled = true
langbot-assets-gateway-host = 0.0.0.0
langbot-assets-gateway-port = 8765
langbot-assets-gateway-request-timeout = 60
langbot-assets-token-ttl = 3600
langbot-assets-input-name = langbot_asset_run_token
```
The runner registers a run-scoped token before calling Dify and removes it when
the run ends. Tool calls without a valid token are rejected by the gateway.
The public Dify MCP provider URL must route to this same gateway instance. The
runner only injects the short-lived token into Dify inputs; it does not create
or update the Dify MCP provider.
### Verify the Flow
Use LangBot Debug Chat or another real LangBot run. Do not rely on Dify Cloud
preview alone, because preview does not automatically receive a live
`langbot_asset_run_token`.
Send a prompt like:
```text
Call LangBot MCP tool langbot_list_assets first, using the injected run_token.
If the tool call succeeds, reply only LANGBOT_DIFY_MCP_OK.
If the token is missing or invalid, reply RUN_TOKEN_FAILED.
```
A passing run should show the sentinel response in LangBot and backend logs
similar to:
```text
assistant: requested tools: langbot_list_assets
```
### Limitations
- Dify Cloud cannot reach `localhost` or a private LAN gateway. Use public HTTPS
for the MCP provider URL.
- Temporary tunnel URLs such as `trycloudflare.com` are only suitable for
testing. When the tunnel stops or changes, the Dify MCP provider URL becomes
invalid.
- The run token is short-lived and run-scoped. It is injected only when LangBot
calls Dify through this runner, then stopped when the run finishes.
- Dify Cloud preview calls usually fail for LangBot MCP tools unless a valid
token from a live LangBot run is supplied manually.
- The Dify app prompt must consistently pass `run_token` to every LangBot MCP
tool call. Missing or invalid tokens are rejected by the gateway.
- Model compatibility matters. The Dify Agent model must support the function
or tool message roles produced by Dify's tool executor.
- The gateway exposes only the assets permitted by the current LangBot run:
current event, history page, knowledge retrieval, tool detail, and tool call.
- The gateway lifetime is tied to the LangBot process and configured port. Port
conflicts or process restarts will break the public MCP endpoint until the
proxy points at the new live gateway.
- Keep the gateway behind HTTPS and avoid logging or exposing run tokens. Tune
`langbot-assets-token-ttl` to the minimum value that still covers expected
Dify tool latency.
## Workflow Inputs
Workflow inputs are passed through `ctx.adapter.extra.params`:
```python
# Runner uses adapter params as Dify inputs
inputs = dict((ctx.adapter.extra or {}).get("params") or {})
```
Legacy input variables are derived from context:
| Variable | Source |
| --- | --- |
| langbot_user_message_text | `ctx.input.to_text()` |
| langbot_session_id | `ctx.conversation.session_id` or `ctx.run_id` |
| langbot_conversation_id | `ctx.state.conversation.get("external.conversation_id")` when it is a Dify UUID |
| langbot_msg_create_time | `ctx.adapter.extra.params["msg_create_time"]` if provided |
## Example Usage
### Chat/Agent Mode with Stateful Session
LangBot host provides state snapshot:
```python
ctx = RunnerContext(
run_id="run_001",
input=AgentInput(text="Hello"),
config={
"base-url": "https://api.dify.ai/v1",
"api-key": "app-xxx",
"app-type": "chat",
},
state=AgentRunState(
conversation={"external.conversation_id": "4f4f8c1b-b1f4-4c9f-9e9f-0f144af69f10"},
),
params={},
)
```
Runner will use the Dify UUID as the Dify conversation ID, maintaining session
continuity.
### Workflow Mode with Custom Inputs
```python
ctx = RunnerContext(
run_id="run_002",
input=AgentInput(text="Process this"),
config={
"base-url": "https://api.dify.ai/v1",
"api-key": "app-yyy",
"app-type": "workflow",
},
params={
"custom_var": "value1",
"workflow_input": "data",
},
)
```
Runner passes `params` to Dify workflow inputs.
## Replaced Runner
Migrated from `dify-service-api` in LangBot.
### Key Changes
1. **Config is static only**: No `conversation_id` in config
2. **State via protocol**: Use `ctx.state` and `state.updated`
3. **Inputs via adapter params**: Use `ctx.adapter.extra.params` for workflow inputs
4. **Scoped state**: `external.conversation_id` with `scope="conversation"`
### Provider identity compatibility (`user-id-source`)
Per-pipeline Runner parameter, not plugin-global configuration. `sender` remains the default.
A `sender` run whose Host actor is missing or empty is refused before any upstream request, instead of minting a per-run provider user; select `legacy-session` explicitly when a conversation-scoped provider user is intended.
Explicit `legacy-session` preserves native provider identity using trusted Host run context only;
missing identity fails before any upstream request, never falls back to the sender or business params.
`legacy-session` uses `conversation.launcher_type` (`group`/`person`) plus `_` plus the exact `conversation.launcher_id`. Host must project the native Query.session launcher into those fields, including interaction/resume runs. Older Hosts that leave these fields empty must be upgraded.
Provider conversation/session persistence remains Runner-owned. Configuration migration does not import old conversation IDs, threads, transcripts, pending forms or files; finish/cancel pending work or perform a separately authorized state migration. Changing identity on an existing stored provider conversation requires an explicit reset/migration decision.
## Dedicated runtime placement
This release is unsigned and dedicated. Historical shared-runtime notes in [SHARED_RUNTIME.md](SHARED_RUNTIME.md) do not grant shared-worker eligibility.