add kia ai chat tools3
CI/CD Pipeline / build-and-deploy (push) Successful in 17s
Details
CI/CD Pipeline / build-and-deploy (push) Successful in 17s
Details
This commit is contained in:
parent
8301329aca
commit
5d64e0ca1a
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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}'")
|
||||
|
||||
# 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,
|
||||
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",
|
||||
},
|
||||
)
|
||||
|
||||
print(f"[KiaAI] HTTP {resp.status_code}")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
def get_kia_model(
|
||||
model: str,
|
||||
reasoning_effort: str | None = None,
|
||||
) -> ChatOpenAI:
|
||||
"""
|
||||
Convenience function to get a KIA AI LangChain model instance.
|
||||
print(f"[KiaAI] Raw response keys: {list(data.keys())}")
|
||||
|
||||
Args:
|
||||
model: Model identifier with optional 'kia/' prefix
|
||||
(e.g. 'kia/gpt-4o', 'kia/o3')
|
||||
reasoning_effort: Optional reasoning effort ('low', 'medium', 'high')
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
usage = data.get("usage") or {}
|
||||
|
||||
Returns:
|
||||
ChatOpenAI configured for KIA AI API
|
||||
"""
|
||||
service = KiaAIService()
|
||||
return service.get_model(model=model, reasoning_effort=reasoning_effort)
|
||||
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
|
||||
|
||||
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue