Initial project import
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from services.account_service import account_service
|
||||
from services.openai_backend_api import OpenAIBackendAPI
|
||||
from services.protocol.conversation import count_message_tokens, count_text_tokens, normalize_messages
|
||||
from services.protocol.openai_v1_chat_complete import collect_chat_content, stream_text_chat_completion
|
||||
|
||||
XML_TOOL_RULE = """Tool output adapter: when calling tools, output ONLY this XML and no prose/markdown:
|
||||
<tool_calls><tool_call><tool_name>TOOL_NAME</tool_name><parameters><PARAM><![CDATA[value]]></PARAM></parameters></tool_call></tool_calls>"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageRequest:
|
||||
backend: OpenAIBackendAPI
|
||||
messages: list[dict[str, Any]]
|
||||
model: str
|
||||
tools: Any = None
|
||||
|
||||
|
||||
def _tool_meta(tool: dict[str, object]) -> tuple[str, str, object]:
|
||||
fn = tool.get("function") if isinstance(tool.get("function"), dict) else {}
|
||||
name = str(tool.get("name") or fn.get("name") or "").strip()
|
||||
desc = str(tool.get("description") or fn.get("description") or "").strip()
|
||||
schema = tool.get("input_schema") or tool.get("parameters") or fn.get("input_schema") or fn.get("parameters") or {}
|
||||
return name, desc, schema
|
||||
|
||||
|
||||
def build_tool_prompt(tools: object) -> str:
|
||||
if not isinstance(tools, list):
|
||||
return ""
|
||||
blocks = []
|
||||
for tool in tools:
|
||||
if not isinstance(tool, dict):
|
||||
continue
|
||||
name, desc, schema = _tool_meta(tool)
|
||||
if name:
|
||||
blocks.append(f"Tool: {name}\nDescription: {desc}\nParameters: {json.dumps(schema, ensure_ascii=False)}")
|
||||
if not blocks:
|
||||
return ""
|
||||
return "Available tools:\n" + "\n".join(blocks) + """
|
||||
|
||||
Tool use rules:
|
||||
- If the user asks to list/read/search files, inspect project state, run a command, or answer from local code, you MUST call a suitable tool first. Do not say you cannot access files.
|
||||
- To call tools, output ONLY XML and no prose/markdown:
|
||||
<tool_calls><tool_call><tool_name>TOOL_NAME</tool_name><parameters><PARAM><![CDATA[value]]></PARAM></parameters></tool_call></tool_calls>
|
||||
- Put parameters under <parameters> using the exact schema names.
|
||||
""".strip()
|
||||
|
||||
|
||||
def merge_system(system: object, extra: str) -> object:
|
||||
system = compact_system(system)
|
||||
if _has_claude_code_system(system):
|
||||
extra = XML_TOOL_RULE
|
||||
if not extra:
|
||||
return system
|
||||
if isinstance(system, str) and system.strip():
|
||||
return f"{system.strip()}\n\n{extra}"
|
||||
if isinstance(system, list):
|
||||
return [*system, {"type": "text", "text": extra}]
|
||||
return extra
|
||||
|
||||
|
||||
def _has_claude_code_system(system: object) -> bool:
|
||||
if isinstance(system, str):
|
||||
return "You are Claude Code" in system
|
||||
if isinstance(system, list):
|
||||
return any(isinstance(item, dict) and "You are Claude Code" in str(item.get("text") or "") for item in system)
|
||||
return False
|
||||
|
||||
|
||||
def compact_system(system: object) -> object:
|
||||
if isinstance(system, str):
|
||||
return _compact_system_text(system)
|
||||
if isinstance(system, list):
|
||||
result = []
|
||||
for item in system:
|
||||
if isinstance(item, dict) and str(item.get("type") or "") == "text":
|
||||
copied = dict(item)
|
||||
copied["text"] = _compact_system_text(str(item.get("text") or ""))
|
||||
result.append(copied)
|
||||
else:
|
||||
result.append(item)
|
||||
return result
|
||||
return system
|
||||
|
||||
|
||||
def _compact_system_text(text: str) -> str:
|
||||
return text or ""
|
||||
|
||||
|
||||
def _compact_message_text(text: str) -> str:
|
||||
return text or ""
|
||||
|
||||
|
||||
def preprocess_payload(payload: dict[str, object], text_mapper: Callable[[str], str] | None = None) -> dict[str, object]:
|
||||
payload["messages"] = preprocess_messages(payload.get("messages"), text_mapper)
|
||||
payload["system"] = merge_system(payload.get("system"), build_tool_prompt(payload.get("tools")))
|
||||
return payload
|
||||
|
||||
|
||||
def message_request(body: dict[str, Any]) -> MessageRequest:
|
||||
payload = preprocess_payload(dict(body))
|
||||
model = str(payload.get("model") or "auto").strip() or "auto"
|
||||
return MessageRequest(
|
||||
backend=OpenAIBackendAPI(access_token=account_service.get_text_access_token(model=model)),
|
||||
messages=normalize_messages(payload.get("messages"), payload.get("system")),
|
||||
model=model,
|
||||
tools=payload.get("tools"),
|
||||
)
|
||||
|
||||
|
||||
def preprocess_messages(messages: object, text_mapper: Callable[[str], str] | None = None) -> object:
|
||||
if not isinstance(messages, list):
|
||||
return messages
|
||||
mapper = text_mapper or (lambda text: text)
|
||||
result = []
|
||||
for message in messages:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
item = dict(message)
|
||||
content = item.get("content")
|
||||
if isinstance(content, str):
|
||||
item["content"] = _compact_message_text(mapper(content))
|
||||
elif isinstance(content, list):
|
||||
item["content"] = [_preprocess_block(block, mapper) for block in content]
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def _preprocess_block(block: object, text_mapper: Callable[[str], str]) -> object:
|
||||
if not isinstance(block, dict):
|
||||
return block
|
||||
block_type = str(block.get("type") or "")
|
||||
if block_type == "text":
|
||||
item = dict(block)
|
||||
item["text"] = _compact_message_text(text_mapper(str(block.get("text") or "")))
|
||||
return item
|
||||
if block_type == "tool_use":
|
||||
return {"type": "text", "text": f"<tool_calls><tool_call><tool_name>{block.get('name') or ''}</tool_name><parameters>{json.dumps(block.get('input') or {}, ensure_ascii=False)}</parameters></tool_call></tool_calls>"}
|
||||
if block_type == "tool_result":
|
||||
return {"type": "text", "text": f"Tool result {block.get('tool_use_id') or ''}: {block.get('content') or ''}"}
|
||||
return block
|
||||
|
||||
|
||||
def message_response(model: str, text: str, input_tokens: int, output_tokens: int, tools: object = None) -> dict[str, object]:
|
||||
content, stop_reason = content_blocks(text, tools)
|
||||
return {
|
||||
"id": f"msg_{uuid.uuid4()}",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": model,
|
||||
"content": content,
|
||||
"stop_reason": stop_reason,
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": input_tokens, "output_tokens": output_tokens},
|
||||
}
|
||||
|
||||
|
||||
def content_blocks(text: str, tools: object = None) -> tuple[list[dict[str, object]], str]:
|
||||
calls = parse_tool_calls(text) if isinstance(tools, list) and tools else []
|
||||
text = strip_tool_markup(text)
|
||||
if calls:
|
||||
content = ([{"type": "text", "text": text}] if text else []) + [{"type": "tool_use", "id": f"toolu_{uuid.uuid4()}", "name": name, "input": args} for name, args in calls]
|
||||
return content, "tool_use"
|
||||
return [{"type": "text", "text": text}], "end_turn"
|
||||
|
||||
|
||||
def strip_tool_markup(text: str) -> str:
|
||||
return re.sub(r"(?is)<tool_calls\b[^>]*>.*?</tool_calls>|<tool_call\b[^>]*>.*?</tool_call>|<function_call\b[^>]*>.*?</function_call>|<invoke\b[^>]*>.*?</invoke>", "", text or "").strip()
|
||||
|
||||
|
||||
def streamable_text(text: str) -> str:
|
||||
text = text or ""
|
||||
match = re.search(r"(?is)<tool_calls\b|<tool_call\b|<function_call\b|<invoke\b", text)
|
||||
return text[:match.start()].rstrip() if match else text
|
||||
|
||||
|
||||
def parse_tool_calls(text: str) -> list[tuple[str, dict[str, object]]]:
|
||||
text = re.sub(r"(?is)```.*?```", "", text or "").strip()
|
||||
blocks = re.findall(r"(?is)<tool_call\b[^>]*>(.*?)</tool_call>|<function_call\b[^>]*>(.*?)</function_call>|<invoke\b[^>]*>(.*?)</invoke>", text)
|
||||
result = []
|
||||
for block in (next((part for part in match if part), "") for match in blocks):
|
||||
name = xml_value(block, "tool_name") or xml_value(block, "name") or xml_value(block, "function")
|
||||
params = xml_value(block, "parameters") or xml_value(block, "input") or xml_value(block, "arguments") or "{}"
|
||||
if name:
|
||||
result.append((name, parse_tool_params(params)))
|
||||
return result
|
||||
|
||||
|
||||
def xml_value(text: str, tag: str) -> str:
|
||||
match = re.search(rf"(?is)<{tag}\b[^>]*>(.*?)</{tag}>", text)
|
||||
if not match:
|
||||
return ""
|
||||
value = match.group(1).strip()
|
||||
cdata = re.fullmatch(r"(?is)<!\[CDATA\[(.*?)]]>", value)
|
||||
return html.unescape(cdata.group(1) if cdata else value).strip()
|
||||
|
||||
|
||||
def parse_tool_params(raw: str) -> dict[str, object]:
|
||||
raw = raw.strip()
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except Exception:
|
||||
return {m.group(1): parse_tool_value(m.group(2)) for m in re.finditer(r"(?is)<([\w.-]+)\b[^>]*>(.*?)</\1>", raw)}
|
||||
|
||||
|
||||
def parse_tool_value(raw: str) -> object:
|
||||
value = xml_value(f"<x>{raw}</x>", "x")
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
|
||||
def stream_events(chunks: Iterable[dict[str, object]], model: str, input_tokens: int, output_tokens: Callable[[str], int], tools: object = None) -> Iterator[dict[str, object]]:
|
||||
message_id = f"msg_{uuid.uuid4()}"
|
||||
created = int(time.time())
|
||||
current_text = ""
|
||||
streamed_text = ""
|
||||
tool_mode = isinstance(tools, list) and bool(tools)
|
||||
tool_started = False
|
||||
text_open = False
|
||||
yield {"type": "message_start", "message": {"id": message_id, "type": "message", "role": "assistant", "model": model, "content": [], "stop_reason": None, "stop_sequence": None, "usage": {"input_tokens": input_tokens, "output_tokens": 0}}}
|
||||
if not tool_mode:
|
||||
text_open = True
|
||||
yield {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}
|
||||
for chunk in chunks:
|
||||
choice = (chunk.get("choices") or [{}])[0]
|
||||
delta = choice.get("delta") or {}
|
||||
text_delta = delta.get("content", "") if isinstance(delta, dict) else ""
|
||||
if text_delta:
|
||||
current_text += text_delta
|
||||
if not tool_started:
|
||||
visible_text = current_text if not tool_mode else streamable_text(current_text)
|
||||
if visible_text.startswith(streamed_text):
|
||||
text_delta = visible_text[len(streamed_text):]
|
||||
if text_delta:
|
||||
if not text_open:
|
||||
text_open = True
|
||||
yield {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}
|
||||
streamed_text = visible_text
|
||||
yield {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": text_delta}}
|
||||
tool_started = tool_mode and visible_text != current_text
|
||||
if choice.get("finish_reason"):
|
||||
content, stop_reason = content_blocks(current_text, tools)
|
||||
if text_open:
|
||||
yield {"type": "content_block_stop", "index": 0}
|
||||
if stop_reason == "tool_use":
|
||||
start_index = 1 if text_open else 0
|
||||
if content and content[0]["type"] == "text":
|
||||
remaining = str(content[0].get("text") or "")[len(streamed_text):]
|
||||
if remaining:
|
||||
if not text_open:
|
||||
yield {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}
|
||||
yield {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": remaining}}
|
||||
if not text_open:
|
||||
yield {"type": "content_block_stop", "index": 0}
|
||||
start_index = 1
|
||||
content = content[1:]
|
||||
yield from _stream_buffered_blocks(content, start_index)
|
||||
yield {"type": "message_delta", "delta": {"stop_reason": stop_reason, "stop_sequence": None}, "usage": {"output_tokens": output_tokens(current_text)}}
|
||||
break
|
||||
yield {"type": "message_stop", "created": created}
|
||||
|
||||
|
||||
def _stream_buffered_blocks(content: list[dict[str, object]], start_index: int = 0) -> Iterator[dict[str, object]]:
|
||||
for offset, block in enumerate(content):
|
||||
index = start_index + offset
|
||||
if block["type"] == "tool_use":
|
||||
start = {"type": "tool_use", "id": block["id"], "name": block["name"], "input": {}}
|
||||
delta = {"type": "input_json_delta", "partial_json": json.dumps(block.get("input") or {}, ensure_ascii=False)}
|
||||
else:
|
||||
start = {"type": "text", "text": ""}
|
||||
delta = {"type": "text_delta", "text": block.get("text") or ""}
|
||||
yield {"type": "content_block_start", "index": index, "content_block": start}
|
||||
yield {"type": "content_block_delta", "index": index, "delta": delta}
|
||||
yield {"type": "content_block_stop", "index": index}
|
||||
|
||||
|
||||
def handle(body: dict[str, Any]) -> dict[str, Any] | Iterator[dict[str, Any]]:
|
||||
request = message_request(body)
|
||||
if body.get("stream"):
|
||||
return stream_events(
|
||||
stream_text_chat_completion(request.backend, request.messages, request.model),
|
||||
request.model,
|
||||
count_message_tokens(request.messages, request.model),
|
||||
lambda text: count_text_tokens(text, request.model),
|
||||
request.tools,
|
||||
)
|
||||
text = collect_chat_content(stream_text_chat_completion(request.backend, request.messages, request.model))
|
||||
return message_response(
|
||||
request.model,
|
||||
text,
|
||||
count_message_tokens(request.messages, request.model),
|
||||
count_text_tokens(text, request.model),
|
||||
request.tools,
|
||||
)
|
||||
Reference in New Issue
Block a user