mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 07:03:58 +08:00
fix: Comprehensive v2.4.0 quality pass — 24 review findings + review agent fixes
Phase A — Critical: - Remove dual timeout stacking in coordinator._send_to_api - Add Gemini-specific asyncio.timeout (sync SDK via to_thread) - Store full text in history for context; cap per-field at 32KB on disk - Fix instance lookup to match by normalized_name - Add Bearer/sk-/x-api-key credential sanitization patterns Phase B — Dead code removal: - Merge async_ask_question/async_process_question into single method - Remove dead is_anthropic flag from coordinator and __init__ - Remove unused DEFAULT_TIMEOUT and API_TIMEOUT constants - Remove redundant _create_history_dir calls - Consolidate async_check_api to use provider registry Phase C — Config flow correctness: - Truncate name before uniqueness check (prevent post-truncation collisions) - Add async_set_unique_id + _abort_if_unique_id_configured - Extract shared _build_parameter_schema for ConfigFlow/OptionsFlow dedup Phase D — UX improvements: - Optimize history write: serialize from memory, single file write - Show last 5 history entries in sensor attributes (was 1) - Return actual error type in ask_question service response - Add dedicated api_key_required error for provider/endpoint changes - Pass config_entry to DataUpdateCoordinator (HA 2024.8+) Phase E — Cleanup: - Extract _apply_structured_output for OpenAI/DeepSeek dedup - Reduce ABSOLUTE_MAX_HISTORY_SIZE to 200 with Final annotation - Remove dead translation keys (queued, invalid_characters) - Migrate to _attr_has_entity_name = True - Add from __future__ import annotations to all modules - Remove redundant api_status sensor attribute - Add missing translation keys (last_model, last_timestamp, etc.) Review agent fixes: - Add archive file cleanup (max 3 archives) to prevent disk exhaustion - Per-entry storage cap (32KB per field) for history on disk
This commit is contained in:
@@ -6,6 +6,8 @@ API Client for HA Text AI.
|
||||
@github: https://github.com/smkrv/ha-text-ai
|
||||
@source: https://github.com/smkrv/ha-text-ai
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -182,6 +184,30 @@ class APIClient:
|
||||
_LOGGER.error("API request failed: %s", str(e))
|
||||
raise HomeAssistantError(f"API request failed: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def _apply_structured_output(
|
||||
payload: Dict[str, Any],
|
||||
structured_output: bool,
|
||||
json_schema: Optional[str],
|
||||
) -> None:
|
||||
"""Apply OpenAI-compatible structured output to payload in-place."""
|
||||
if not (structured_output and json_schema):
|
||||
return
|
||||
import json
|
||||
try:
|
||||
schema = json.loads(json_schema)
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "structured_response",
|
||||
"strict": True,
|
||||
"schema": schema,
|
||||
},
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.warning("Invalid JSON schema: %s. Falling back to json_object.", e)
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
async def _create_deepseek_completion(
|
||||
self,
|
||||
model: str,
|
||||
@@ -198,26 +224,9 @@ class APIClient:
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
# Add structured output format if enabled (DeepSeek is OpenAI-compatible)
|
||||
if structured_output and json_schema:
|
||||
try:
|
||||
import json
|
||||
schema = json.loads(json_schema)
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "structured_response",
|
||||
"strict": True,
|
||||
"schema": schema
|
||||
}
|
||||
}
|
||||
_LOGGER.debug("DeepSeek structured output enabled with schema")
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.warning("Invalid JSON schema provided: %s. Falling back to json_object mode.", e)
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
self._apply_structured_output(payload, structured_output, json_schema)
|
||||
|
||||
data = await self._make_request(url, payload)
|
||||
return {
|
||||
@@ -250,24 +259,7 @@ class APIClient:
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
|
||||
# Add structured output format if enabled
|
||||
if structured_output and json_schema:
|
||||
try:
|
||||
import json
|
||||
schema = json.loads(json_schema)
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "structured_response",
|
||||
"strict": True,
|
||||
"schema": schema
|
||||
}
|
||||
}
|
||||
_LOGGER.debug("OpenAI structured output enabled with schema")
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.warning("Invalid JSON schema provided: %s. Falling back to json_object mode.", e)
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
self._apply_structured_output(payload, structured_output, json_schema)
|
||||
|
||||
data = await self._make_request(url, payload)
|
||||
return {
|
||||
@@ -469,7 +461,10 @@ class APIClient:
|
||||
)
|
||||
return chat.send_message(last_user_msg)
|
||||
|
||||
response = await asyncio.to_thread(generate_content)
|
||||
# Gemini uses sync SDK via to_thread, so needs its own timeout
|
||||
# (aiohttp ClientTimeout doesn't apply here)
|
||||
async with asyncio.timeout(self.api_timeout):
|
||||
response = await asyncio.to_thread(generate_content)
|
||||
|
||||
# Extract response text
|
||||
def extract_response():
|
||||
|
||||
Reference in New Issue
Block a user