add kia ai chat tools3
CI/CD Pipeline / build-and-deploy (push) Successful in 17s Details

This commit is contained in:
vahidrezvani 2026-02-25 12:12:13 +03:30
parent 8301329aca
commit 5d64e0ca1a
2 changed files with 86 additions and 62 deletions

View File

@ -56,7 +56,8 @@ async def web_search_tool(query: str) -> str:
def get_model(model: str):
if model.startswith("kia/"):
return get_kia_model(model)
# KIA models use KiaAIService.invoke() directly in call_model — no LangChain model needed
return None
if "claude" in model:
# return ChatAnthropic(model=model, api_key=settings.anthropic_api_key)
@ -176,10 +177,10 @@ def chatbot_workflow(bot, now):
print(f"[chatbot_workflow] START | model='{bot.model}' | is_kia={is_kia_model} | web_search={bot.web_search}")
model = get_model(model=bot.model)
print(f"[chatbot_workflow] Model initialized: {type(model).__name__}")
print(f"[chatbot_workflow] Model initialized: {type(model).__name__ if model else 'KiaAIService (direct http)'}")
trimmer = get_trimmer()
if bot.web_search:
if bot.web_search and model is not None:
model = model.bind_tools([web_search_tool])
async def call_model(state: MessagesState):
@ -203,12 +204,14 @@ def chatbot_workflow(bot, now):
)
] + trimmed_messages
# KIA API returns choices=null in streaming chunks which breaks LangChain.
# Even with streaming=False, ainvoke() still uses _astream() internally in async path.
# asyncio.to_thread forces sync model.invoke() which calls the real non-streaming endpoint.
# KIA models must bypass LangChain entirely:
# both ainvoke() and invoke() internally use _astream()/_stream() in this
# version of LangChain which fails on KIA's choices=null streaming chunks.
# Direct httpx call is the only reliable path.
if is_kia:
print(f"[KiaAI] Calling model via asyncio.to_thread (non-streaming) | model='{bot.model}'")
response = await asyncio.to_thread(model.invoke, input_messages)
from .kiaai import KiaAIService
print(f"[KiaAI] Calling via KiaAIService.invoke() | model='{bot.model}'")
response = await KiaAIService().invoke(model=bot.model, messages=input_messages)
else:
response = await model.ainvoke(input=input_messages, **kwargs)

View File

@ -1,80 +1,101 @@
from langchain_openai import ChatOpenAI
import httpx
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
SystemMessage,
ToolMessage,
)
from ...config import settings
KIA_BASE_URL = "https://api.kie.ai"
# Models that support reasoning_effort parameter
REASONING_MODELS = {"gpt-5-2", "claude-sonnet-4-5", "claude-opus-4-5", "gemini-3-pro", "gemini-2.5-flash","gemini-2.5-pro"}
def _to_openai_messages(messages: list[BaseMessage]) -> list[dict]:
"""Convert LangChain messages to OpenAI-compatible dict format."""
result = []
for msg in messages:
if isinstance(msg, SystemMessage):
result.append({"role": "system", "content": msg.content})
elif isinstance(msg, HumanMessage):
result.append({"role": "user", "content": msg.content})
elif isinstance(msg, AIMessage):
result.append({"role": "assistant", "content": msg.content})
elif isinstance(msg, ToolMessage):
result.append({"role": "tool", "content": msg.content, "tool_call_id": msg.tool_call_id})
else:
result.append({"role": "user", "content": str(msg.content)})
return result
class KiaAIService:
"""
Service for interacting with KIA AI models via OpenAI-compatible API.
Service for calling KIA AI models directly via httpx (non-streaming).
KIA AI endpoint pattern:
https://api.kie.ai/{modelName}/v1/chat/completions
Bypasses LangChain's streaming infrastructure entirely to avoid the
'TypeError: object of type NoneType has no len()' bug caused by
KIA API returning chunks with choices=null.
Usage:
service = KiaAIService()
model = service.get_model("kia/gpt-4o")
model = service.get_model("kia/o3", reasoning_effort="high")
Endpoint pattern:
POST https://api.kie.ai/{modelName}/v1/chat/completions
"""
def get_model(
self,
model: str,
reasoning_effort: str | None = None,
) -> ChatOpenAI:
async def invoke(self, model: str, messages: list[BaseMessage]) -> AIMessage:
"""
Returns a LangChain-compatible ChatOpenAI model configured for KIA AI API.
Call KIA API directly and return an AIMessage.
Args:
model: Model identifier with optional 'kia/' prefix
(e.g. 'kia/gpt-4o', 'kia/claude-3-5-sonnet', 'kia/o3')
reasoning_effort: Reasoning effort level for supported models.
One of: 'low', 'medium', 'high'
model: Model name with optional 'kia/' prefix (e.g. 'kia/gpt-5-2')
messages: List of LangChain BaseMessage objects
Returns:
ChatOpenAI instance pointing to https://api.kie.ai/{modelName}/v1
AIMessage with content and usage_metadata populated
"""
model_name = model.removeprefix("kia/")
base_url = f"{KIA_BASE_URL}/{model_name}/v1"
url = f"{KIA_BASE_URL}/{model_name}/v1/chat/completions"
model_kwargs: dict = {}
payload = {
"model": model_name,
"messages": _to_openai_messages(messages),
"stream": False,
}
# Add reasoning_effort for models that support it
if reasoning_effort and model_name in REASONING_MODELS:
model_kwargs["reasoning_effort"] = reasoning_effort
print(f"[KiaAI] POST {url} | messages_count={len(messages)}")
print(f"[KiaAI] Initializing model='{model_name}' | base_url='{base_url}' | reasoning_effort='{reasoning_effort}'")
async with httpx.AsyncClient(timeout=180.0) as client:
resp = await client.post(
url,
json=payload,
headers={
"Authorization": f"Bearer {settings.kia_api_key}",
"Content-Type": "application/json",
},
)
# streaming=False is REQUIRED: KIA API returns chunks with choices=null
# which causes TypeError in LangChain's _convert_chunk_to_generation_chunk.
# Disabling streaming forces LangChain to use the non-streaming endpoint.
return ChatOpenAI(
model=model_name,
api_key=settings.kia_api_key,
base_url=base_url,
model_kwargs=model_kwargs,
streaming=False,
)
print(f"[KiaAI] HTTP {resp.status_code}")
resp.raise_for_status()
data = resp.json()
print(f"[KiaAI] Raw response keys: {list(data.keys())}")
content = data["choices"][0]["message"]["content"]
usage = data.get("usage") or {}
print(f"[KiaAI] content='{content[:200]}'")
print(f"[KiaAI] usage={usage}")
ai_msg = AIMessage(content=content)
ai_msg.usage_metadata = {
"input_tokens": usage.get("prompt_tokens", 0),
"output_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0),
}
return ai_msg
def get_kia_model(
model: str,
reasoning_effort: str | None = None,
) -> ChatOpenAI:
"""
Convenience function to get a KIA AI LangChain model instance.
Args:
model: Model identifier with optional 'kia/' prefix
(e.g. 'kia/gpt-4o', 'kia/o3')
reasoning_effort: Optional reasoning effort ('low', 'medium', 'high')
Returns:
ChatOpenAI configured for KIA AI API
"""
service = KiaAIService()
return service.get_model(model=model, reasoning_effort=reasoning_effort)
# Keep get_kia_model for compatibility — but actual chat calls should use KiaAIService.invoke()
def get_kia_model(model: str):
"""Compatibility stub — returns None for KIA models since they use direct HTTP."""
return None