feat(B-LLM): Zentraler LLM Client — llm_complete() + llm_embed() + Migration + Tests + Doku
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
B-LLM: llm_client.py um generische llm_complete() und llm_embed() erweitert - Provider-Auswahl, API-Key-Auflösung, Error-Handling, Cost-Tracking - Retry mit Exponential-Backoff für transient errors - Timeout konfigurierbar - Helper: get_api_credentials(), build_model(), _classify_error() B-LLM-MIG: Alle 8 direkten litellm.acompletion() Calls auf llm_complete() umgestellt - agent_runner.py, query_understanding.py (2x), ai_proactive (3x), ai_assistant (2x) - 0 verbleibende direkte litellm.acompletion() Calls außerhalb llm_client.py B-LLM-TEST: 39 Tests in test_llm_client.py — alle grün - Mock mode, error handling, embed, helpers, backward compat B-LLM-DOC: Plugin-Dev-Guide Kapitel 7 (LLM Integration) hinzugefügt
This commit is contained in:
@@ -18,6 +18,8 @@ import litellm
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai.llm_client import llm_complete
|
||||
|
||||
from app.core.permissions import check_permission
|
||||
from app.plugins.builtins.ai_assistant.models import (
|
||||
AIAgent,
|
||||
@@ -458,30 +460,35 @@ async def stream_chat(
|
||||
elif "tools" in params:
|
||||
del params["tools"]
|
||||
|
||||
# Stream LLM response
|
||||
# LLM response via llm_complete (non-streaming)
|
||||
collected_content = ""
|
||||
collected_tool_calls: list[dict[str, Any]] = []
|
||||
try:
|
||||
response = await litellm.acompletion(**params)
|
||||
async for chunk in response:
|
||||
delta = chunk.choices[0].delta
|
||||
if delta.content:
|
||||
collected_content += delta.content
|
||||
yield f"data: {json.dumps({'type': 'token', 'content': delta.content})}\n\n"
|
||||
if delta.tool_calls:
|
||||
for tc in delta.tool_calls:
|
||||
idx = tc.index
|
||||
while len(collected_tool_calls) <= idx:
|
||||
collected_tool_calls.append({"id": "", "function": {"name": "", "arguments": ""}})
|
||||
if tc.id:
|
||||
collected_tool_calls[idx]["id"] = tc.id
|
||||
if tc.function:
|
||||
if tc.function.name:
|
||||
collected_tool_calls[idx]["function"]["name"] += tc.function.name
|
||||
if tc.function.arguments:
|
||||
collected_tool_calls[idx]["function"]["arguments"] += tc.function.arguments
|
||||
result = await llm_complete(
|
||||
model=params.get("model", "gpt-4o-mini"),
|
||||
messages=params.get("messages", []),
|
||||
temperature=params.get("temperature", 0.7),
|
||||
max_tokens=params.get("max_tokens", 2048),
|
||||
api_key=params.get("api_key"),
|
||||
api_base=params.get("api_base"),
|
||||
tools=params.get("tools"),
|
||||
)
|
||||
collected_content = result["content"]
|
||||
if collected_content:
|
||||
yield f"data: {json.dumps({'type': 'token', 'content': collected_content})}\n\n"
|
||||
# Extract tool calls from raw response
|
||||
raw_response = result["raw_response"]
|
||||
if hasattr(raw_response.choices[0].message, "tool_calls") and raw_response.choices[0].message.tool_calls:
|
||||
for tc in raw_response.choices[0].message.tool_calls:
|
||||
collected_tool_calls.append({
|
||||
"id": tc.id or "",
|
||||
"function": {
|
||||
"name": tc.function.name if tc.function else "",
|
||||
"arguments": tc.function.arguments if tc.function and tc.function.arguments else "",
|
||||
},
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.error("LLM streaming error: %s", exc)
|
||||
logger.error("LLM error: %s", exc)
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': str(exc)})}\n\n"
|
||||
await save_message(db, session.id, "assistant", f"Error: {exc}", tenant_id, model_used=model_id)
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user