Files
life-echo/api/app/adapters/llm/deepseek.py

109 lines
3.4 KiB
Python
Raw Normal View History

"""DeepSeek / OpenAI-compatible LLM adapter — implements LLMProvider port."""
from collections.abc import AsyncIterator
from langchain_openai import ChatOpenAI
class DeepSeekLLMProvider:
2026-03-20 15:15:35 +08:00
"""LangChain-based LLM adapter for DeepSeek and OpenAI-compatible APIs.
`langchain_llm` Agent / 任务同步调用若需 JSON object 模式请用
`app.core.langchain_llm.bind_json_object_mode` `invoke_json_object` /
`ainvoke_json_object` `api/docs/llm-json-mode.md`勿使用已废弃的
2026-03-20 15:15:35 +08:00
`bind(model_kwargs={"response_format": ...})`
"""
def __init__(
self,
api_key: str,
base_url: str = "https://api.deepseek.com",
model: str = "deepseek-v4-flash",
temperature: float = 0.7,
*,
extra_body: dict | None = None,
):
self._default_model = model
self._default_temperature = temperature
kwargs: dict = {
"temperature": temperature,
"model": model,
"api_key": api_key,
}
if extra_body:
kwargs["extra_body"] = extra_body
if base_url:
cleaned = base_url.rstrip("/")
for suffix in ("/v1/chat/completions", "/v1"):
if cleaned.endswith(suffix):
cleaned = cleaned[: -len(suffix)]
kwargs["base_url"] = cleaned
self._llm = ChatOpenAI(**kwargs)
@property
def langchain_llm(self) -> ChatOpenAI:
"""Expose underlying ChatOpenAI for LangChain agent interop (Phase 2 will remove)."""
return self._llm
async def complete(
self,
messages: list[dict],
*,
temperature: float | None = None,
model: str | None = None,
max_tokens: int | None = None,
) -> str:
llm = self._get_llm(temperature, model, max_tokens)
lc_messages = _to_langchain_messages(messages)
result = await llm.ainvoke(lc_messages)
return str(result.content)
async def stream(
self,
messages: list[dict],
*,
temperature: float | None = None,
model: str | None = None,
max_tokens: int | None = None,
) -> AsyncIterator[str]:
llm = self._get_llm(temperature, model, max_tokens)
lc_messages = _to_langchain_messages(messages)
async for chunk in llm.astream(lc_messages):
if chunk.content:
yield str(chunk.content)
def _get_llm(
self,
temperature: float | None,
model: str | None,
max_tokens: int | None = None,
):
if temperature is None and model is None and max_tokens is None:
return self._llm
kwargs: dict = {}
if temperature is not None:
kwargs["temperature"] = temperature
if model is not None:
kwargs["model"] = model
if max_tokens is not None:
kwargs["max_tokens"] = max_tokens
return self._llm.bind(**kwargs) if kwargs else self._llm
def _to_langchain_messages(messages: list[dict]) -> list:
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
2026-03-19 14:36:14 +08:00
mapping = {
"system": SystemMessage,
"human": HumanMessage,
"user": HumanMessage,
"ai": AIMessage,
"assistant": AIMessage,
}
result = []
for msg in messages:
cls = mapping.get(msg.get("role", ""), HumanMessage)
result.append(cls(content=msg.get("content", "")))
return result