feat(B-LLM): Zentraler LLM Client — llm_complete() + llm_embed() + Migration + Tests + Doku
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:
Agent Zero
2026-08-13 16:22:05 +02:00
parent 3d8210637e
commit e3ca3b3d28
12 changed files with 1230 additions and 234 deletions
@@ -117,8 +117,6 @@ async def search_related_handler(arguments: dict[str, Any], context: dict[str, A
async def summarize_mail_thread_handler(arguments: dict[str, Any], context: dict[str, Any]) -> str:
"""Summarize a mail thread using LLM."""
try:
import litellm
db, tenant_id, _ = await _get_db_and_tenant(context)
thread_id = arguments["thread_id"]
limit = arguments.get("limit", 20)
@@ -139,7 +137,9 @@ async def summarize_mail_thread_handler(arguments: dict[str, Any], context: dict
for m in mails
)
response = await litellm.acompletion(
from app.ai.llm_client import llm_complete
result = await llm_complete(
model="gpt-4o-mini",
messages=[
{
@@ -151,7 +151,7 @@ async def summarize_mail_thread_handler(arguments: dict[str, Any], context: dict
temperature=0.3,
max_tokens=300,
)
summary = response.choices[0].message.content or ""
summary = result["content"] or ""
return json.dumps({"summary": summary, "count": len(mails)}, default=str)
except Exception as e:
logger.exception("summarize_mail_thread_handler failed")
+19 -23
View File
@@ -16,7 +16,7 @@ import logging
import uuid
from typing import Any
import litellm
from app.ai.llm_client import llm_complete
from sqlalchemy import select
from app.core.db import create_db_session
@@ -201,29 +201,25 @@ async def deep_analysis(
model_parts = model.split("/", 1)
model = f"{provider_type}/{model_parts[-1]}"
litellm_kwargs: dict[str, Any] = dict(
model=model,
messages=[
{"role": "system", "content": DEEP_SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(
extended_context, default=str, ensure_ascii=False
),
},
],
temperature=0.3,
max_tokens=1500,
response_format={"type": "json_object"},
)
if api_key:
litellm_kwargs["api_key"] = api_key
if api_base:
litellm_kwargs["api_base"] = api_base
try:
response = await litellm.acompletion(**litellm_kwargs)
content = response.choices[0].message.content
result = await llm_complete(
model=model,
messages=[
{"role": "system", "content": DEEP_SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(
extended_context, default=str, ensure_ascii=False
),
},
],
temperature=0.3,
max_tokens=1500,
response_format={"type": "json_object"},
api_key=api_key,
api_base=api_base,
)
content = result["content"]
if not content:
return
# Strip markdown code fences if present
+17 -20
View File
@@ -15,6 +15,7 @@ from datetime import UTC, datetime, timedelta
from typing import Any
import litellm
from app.ai.llm_client import llm_complete
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
@@ -421,27 +422,23 @@ async def generate_suggestion(
model_parts = model.split("/", 1)
model = f"{provider_type}/{model_parts[-1]}"
litellm_kwargs: dict[str, Any] = dict(
model=model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(context_data, default=str, ensure_ascii=False),
},
],
temperature=0.3,
max_tokens=1500,
response_format={"type": "json_object"},
)
if api_key:
litellm_kwargs["api_key"] = api_key
if api_base:
litellm_kwargs["api_base"] = api_base
try:
response = await litellm.acompletion(**litellm_kwargs)
content = response.choices[0].message.content
result = await llm_complete(
model=model,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(context_data, default=str, ensure_ascii=False),
},
],
temperature=0.3,
max_tokens=1500,
response_format={"type": "json_object"},
api_key=api_key,
api_base=api_base,
)
content = result["content"]
if not content:
return None
# Strip markdown code fences if present (e.g. ```json ... ```)