mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 18:55:07 +03:00
145 lines
6.8 KiB
Python
145 lines
6.8 KiB
Python
"""Claude's native stream-json/control protocol; message handling adapted from Computer."""
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import json
|
|
from uuid import uuid4
|
|
|
|
from . import reasoning_effort
|
|
|
|
|
|
async def run(transport, output, prompt, images, options):
|
|
async def send(message):
|
|
await transport.write(json.dumps(message) + '\n')
|
|
|
|
messages = transport.read_messages()
|
|
|
|
async def request(control):
|
|
request_id = uuid4().hex
|
|
await send({'type': 'control_request', 'request_id': request_id, 'request': control})
|
|
async with asyncio.timeout(60):
|
|
async for message in messages:
|
|
if (
|
|
message.get('type') == 'control_response'
|
|
and message.get('response', {}).get('request_id') == request_id
|
|
):
|
|
if message['response'].get('subtype') == 'error':
|
|
raise ValueError(message['response'].get('error', 'Claude request failed'))
|
|
return
|
|
raise ValueError('Claude ended before replying')
|
|
|
|
await request({'subtype': 'initialize'})
|
|
effort = reasoning_effort(options)
|
|
if effort is not None:
|
|
await request({'subtype': 'apply_flag_settings', 'settings': {'effortLevel': effort}})
|
|
content = [{'type': 'text', 'text': prompt}]
|
|
for image in images:
|
|
prefix, data = image.split(',', 1)
|
|
content.append(
|
|
{'type': 'image', 'source': {'type': 'base64', 'media_type': prefix[5:].split(';')[0], 'data': data}}
|
|
)
|
|
await send(
|
|
{
|
|
'type': 'user',
|
|
'message': {'role': 'user', 'content': content},
|
|
'parent_tool_use_id': None,
|
|
'session_id': options.get('resume') or 'default',
|
|
}
|
|
)
|
|
streamed_text = streamed_thinking = False
|
|
try:
|
|
async for message in messages:
|
|
kind = message.get('type')
|
|
if kind == 'system' and message.get('subtype') == 'init':
|
|
output.state['session_id'] = message['session_id']
|
|
await output.flush(force=True, persist=True)
|
|
elif kind == 'control_request':
|
|
request = message['request']
|
|
if request.get('subtype') != 'can_use_tool':
|
|
await send(
|
|
{
|
|
'type': 'control_response',
|
|
'response': {
|
|
'subtype': 'error',
|
|
'request_id': message['request_id'],
|
|
'error': 'Unsupported control request',
|
|
},
|
|
}
|
|
)
|
|
continue
|
|
name, arguments = request['tool_name'], request['input']
|
|
question = name == 'AskUserQuestion'
|
|
presented = arguments
|
|
if question:
|
|
presented = {
|
|
'questions': [
|
|
{**q, 'id': q.get('question', str(i))} for i, q in enumerate(arguments.get('questions', []))
|
|
]
|
|
}
|
|
resolution = await output.approve('ask_user' if question else name, presented, message)
|
|
if resolution['action'] == 'reject' or resolution.get('timed_out'):
|
|
response = {'behavior': 'deny', 'message': 'Rejected by the user'}
|
|
else:
|
|
answers = {
|
|
key: (value.get('text', '') if value.get('type') == 'other' else value.get('label', ''))
|
|
if isinstance(value, dict)
|
|
else value
|
|
for key, value in (resolution.get('answers') or {}).items()
|
|
}
|
|
updated = {**arguments, 'answers': answers} if question else arguments
|
|
response = {'behavior': 'allow', 'updatedInput': updated}
|
|
await send(
|
|
{
|
|
'type': 'control_response',
|
|
'response': {'subtype': 'success', 'request_id': message['request_id'], 'response': response},
|
|
}
|
|
)
|
|
elif kind == 'stream_event':
|
|
event = message.get('event') or {}
|
|
if event.get('type') == 'message_start':
|
|
streamed_text = streamed_thinking = False
|
|
if event.get('type') == 'content_block_delta':
|
|
delta = event.get('delta') or {}
|
|
if delta.get('type') == 'text_delta':
|
|
streamed_text = True
|
|
await output.text(delta.get('text', ''))
|
|
elif delta.get('type') == 'thinking_delta':
|
|
streamed_thinking = True
|
|
await output.text(delta.get('thinking', ''), reasoning=True)
|
|
elif kind == 'assistant':
|
|
for block in message.get('message', {}).get('content', []):
|
|
if block['type'] == 'text' and not streamed_text:
|
|
await output.text(block['text'])
|
|
elif block['type'] == 'thinking' and not streamed_thinking:
|
|
await output.text(block['thinking'], reasoning=True)
|
|
elif block['type'] == 'tool_use':
|
|
await output.tool(block['id'], block['name'], block['input'], native=block)
|
|
elif kind == 'user':
|
|
blocks = message.get('message', {}).get('content', [])
|
|
for block in blocks if isinstance(blocks, list) else []:
|
|
if block.get('type') == 'tool_result':
|
|
previous = next((i for i in output.items if i.get('call_id') == block['tool_use_id']), {})
|
|
await output.tool(
|
|
block['tool_use_id'],
|
|
previous.get('name', 'agent_tool'),
|
|
json.loads(previous.get('arguments', '{}')),
|
|
native=block,
|
|
result=block.get('content'),
|
|
done=True,
|
|
)
|
|
elif kind == 'result':
|
|
output.state['session_id'] = message['session_id']
|
|
output.usage = message.get('usage')
|
|
if message.get('is_error'):
|
|
raise ValueError(
|
|
message.get('result') or '; '.join(message.get('errors') or []) or 'Claude Code turn failed'
|
|
)
|
|
return
|
|
raise ValueError('Claude Code ended without a turn result')
|
|
except asyncio.CancelledError:
|
|
with contextlib.suppress(Exception):
|
|
await asyncio.wait_for(
|
|
send({'type': 'control_request', 'request_id': uuid4().hex, 'request': {'subtype': 'interrupt'}}), 5
|
|
)
|
|
raise
|