Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions nodes/src/nodes/webhook/IEndpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ async def _startup(self):
# These should NOT be replacable strings!!!
info = {
'button-text': 'Chat now',
'button-link': '{host}/chat?auth={public_auth}',
'button-link': '{host}/chat/{project_id}/{source}?auth={public_auth}',
'url-text': 'Chat interface URL',
'url-link': '{host}/chat',
'url-link': '{host}/chat/{project_id}/{source}',
'auth-text': 'Public Authorization Key',
'auth-key': '{public_auth}',
'token-text': 'Private Token',
Expand All @@ -88,9 +88,9 @@ async def _startup(self):
# These should NOT be replacable strings!!!
info = {
'button-text': 'Drop now',
'button-link': '{host}/dropper?auth={public_auth}',
'button-link': '{host}/dropper/{project_id}/{source}?auth={public_auth}',
'url-text': 'Dropper interface URL',
'url-link': '{host}/dropper',
'url-link': '{host}/dropper/{project_id}/{source}',
'auth-text': 'Public Authorization Key',
'auth-key': '{public_auth}',
'token-text': 'Private Token',
Expand All @@ -115,7 +115,7 @@ async def _startup(self):
}
info = {
'url-text': url_text_map[self.endpoint.logicalType],
'url-link': '{host}/webhook',
'url-link': '{host}/webhook/{project_id}/{source}',
'auth-text': 'Public Authorization Key',
'auth-key': '{public_auth}',
'token-text': 'Private Token',
Expand Down
2 changes: 1 addition & 1 deletion nodes/src/nodes/webhook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Stands up its own HTTP endpoint and forwards incoming data into the attached pip

The server is a **FastAPI / Uvicorn** wrapper (`ai.web.WebServer`). The engine passes the bind address on the command line (`--data_host`, `--data_port`, defaulting to `localhost:5567`); the node loads the `data` module, which registers a `/task/data` websocket as the data plane between the public endpoint and the pipeline. The node keeps running until the pipeline is stopped, the source task completes only when the server exits.

After the pipeline starts, the Project Log displays the interface URL, the public authorization key, and the private token, so callers know how to reach the endpoint (the chat link form is `{host}/chat?auth={public_auth}`).
After the pipeline starts, the Project Log displays a stable, pipe-specific interface URL, the public authorization key, and the private token, so callers know how to reach the endpoint. The URL forms are `{host}/chat/{project_id}/{source}?auth={public_auth}`, `{host}/dropper/{project_id}/{source}?auth={public_auth}`, and `{host}/webhook/{project_id}/{source}`. The legacy `/chat`, `/dropper`, `/webhook`, and `/task/data` URLs continue to work for existing integrations.

---

Expand Down
14 changes: 14 additions & 0 deletions packages/ai/src/ai/modules/task/task_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,14 @@ def _update_status(self, message: Dict[str, Any]) -> None:
# Handle string notes - simple replacement
note = note.replace('{token}', self.token)
note = note.replace('{public_auth}', self.public_auth)
# getattr: partially-initialized tasks (and test stubs)
# may not carry the identity attributes at all.
project_id = getattr(self, 'project_id', None)
source = getattr(self, 'source', None)
if project_id is not None:
note = note.replace('{project_id}', str(project_id))
if source is not None:
note = note.replace('{source}', str(source))
self._status.notes.append(note)
elif isinstance(note, dict):
# Handle dict notes - walk through and replace in all string values
Expand All @@ -988,6 +996,12 @@ def _update_status(self, message: Dict[str, Any]) -> None:
# Replace tokens in string values
value = value.replace('{token}', self.token)
value = value.replace('{public_auth}', self.public_auth)
project_id = getattr(self, 'project_id', None)
source = getattr(self, 'source', None)
if project_id is not None:
value = value.replace('{project_id}', str(project_id))
if source is not None:
value = value.replace('{source}', str(source))
processed_note[key] = value
self._status.notes.append(processed_note)
else:
Expand Down
2 changes: 2 additions & 0 deletions packages/ai/src/ai/modules/task_http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ def initModule(server: WebServer, config: Dict[str, Any]):
# Preferred routes (POST)
server.add_route('/task', task_Execute, methods=['POST'])
server.add_route('/task/data', task_Data, methods=['POST'])
server.add_route('/task/data/{project_id}/{source}', task_Data, methods=['POST'])
server.add_route('/task', task_Status, methods=['GET'])
server.add_route('/task', task_Cancel, methods=['DELETE'])

# Alias to /task/data
server.add_route('/webhook', task_Data, methods=['POST'])
server.add_route('/webhook/{project_id}/{source}', task_Data, methods=['POST'])

# Deprecated routes (PUT) - kept for backward compatibility
server.add_route('/task', task_Execute, methods=['PUT'], deprecated=True)
Expand Down
13 changes: 13 additions & 0 deletions packages/ai/src/ai/modules/task_http/task_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,19 @@ async def task_Data(
client = None

try:
if token is None:
project_id = request.path_params.get('project_id')
source = request.path_params.get('source')
if project_id is not None and source is not None:
try:
control = request.app.state.task.get_task_control_by_project(project_id, source)
token = control.token
except RuntimeError:
# Documented missing-task case ('Your pipeline is not
# running') — fall back to token=None so the request takes
# the normal auth/404 path. Anything else propagates.
pass
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Get the WebServer instance from application state
server: WebServer = request.app.state.server
port = server.get_port()
Expand Down
17 changes: 11 additions & 6 deletions packages/docs/content-static/examples/webhook-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Save this as `chat.pipe`:

```json
{
"project_id": "webhook-example",
"nodes": [
{
"id": "source_1",
Expand Down Expand Up @@ -57,7 +58,7 @@ Output:

```text
Webhook ready - system is ready to accept requests
URL: http://localhost:5567/task/data
URL: http://localhost:5567/webhook/webhook-example/source_1
Auth: <public-auth-key>
Token: <private-token>
```
Expand All @@ -67,7 +68,7 @@ Keep the terminal open — the pipeline runs until you stop it.
## Send a question

```bash
curl -X POST http://localhost:5567/task/data \
curl -X POST "http://localhost:5567/webhook/webhook-example/source_1" \
-H "Authorization: Bearer <public-auth-key>" \
-H "Content-Type: text/plain" \
-d "What is the capital of France?"
Expand All @@ -92,13 +93,17 @@ Restart the pipeline. The engine prints a chat URL:

```text
Chat ready - system is ready to accept questions
URL: http://localhost:5567/chat?auth=<public-auth-key>
URL: http://localhost:5567/chat/webhook-example/source_1?auth=<public-auth-key>
```

Open that URL in a browser and start typing. The `?auth=` query parameter is a
convenience for the browser; the endpoint also accepts the key in an
`Authorization: Bearer <public-auth-key>` header, the same scheme the webhook
endpoint uses.
convenience for supplying the credential to the browser; the Chat UI stores it
in session storage for subsequent page loads. Webhook requests use the same key
in an `Authorization: Bearer <public-auth-key>` header.

Each displayed URL includes the project and source identifiers, so it remains
specific to this pipeline. The legacy `/webhook`, `/task/data`, `/chat`, and
`/dropper` URLs continue to work for existing integrations.

## Add a system prompt

Expand Down
Loading