hjg 账号默认使用 DeepSeek Pro AI 配置
This commit is contained in:
@@ -125,7 +125,7 @@ async def analyze_financials(request: Request, req: AnalyzeRequest):
|
|||||||
data_dir = request.app.state.repo.store.data_dir
|
data_dir = request.app.state.repo.store.data_dir
|
||||||
|
|
||||||
async def stream_gen():
|
async def stream_gen():
|
||||||
async for chunk in analyze_financials_stream(data_dir, req.symbol, req.focus):
|
async for chunk in analyze_financials_stream(data_dir, req.symbol, req.focus, username=request.state.username):
|
||||||
yield chunk + "\n"
|
yield chunk + "\n"
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async def analyze_market(request: Request, req: AnalyzeRequest):
|
|||||||
raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}")
|
raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}")
|
||||||
|
|
||||||
async def stream_gen():
|
async def stream_gen():
|
||||||
async for chunk in recap_market_stream(repo, as_of, req.focus):
|
async for chunk in recap_market_stream(repo, as_of, req.focus, username=request.state.username):
|
||||||
yield chunk + "\n"
|
yield chunk + "\n"
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ async def analyze_rotation(request: Request, req: AnalyzeRequest):
|
|||||||
async def stream_gen():
|
async def stream_gen():
|
||||||
async for chunk in analyze_rotation_stream(
|
async for chunk in analyze_rotation_stream(
|
||||||
repo, days, req.focus,
|
repo, days, req.focus,
|
||||||
|
username=request.state.username,
|
||||||
):
|
):
|
||||||
yield chunk + "\n"
|
yield chunk + "\n"
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,38 @@ class TickflowKeyIn(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def _get_ai_config(username: str | None, key: str, default: str = "") -> str:
|
def _get_ai_config(username: str | None, key: str, default: str = "") -> str:
|
||||||
"""读 AI 配置: 用户级优先, 无则全局。"""
|
"""读 AI 配置: 用户级优先, 无则全局。hjg 账号回退到内置 DeepSeek Pro。"""
|
||||||
if username:
|
if username:
|
||||||
val = secrets_store.load_ai_config(username).get(key)
|
val = secrets_store.load_ai_config(username).get(key)
|
||||||
if val:
|
if val:
|
||||||
return val
|
return val
|
||||||
|
if username == "hjg":
|
||||||
|
if key == "ai_provider":
|
||||||
|
return "openai_compat"
|
||||||
|
if key == "ai_base_url":
|
||||||
|
return "https://api.deepseek.com/v1"
|
||||||
|
if key == "ai_model":
|
||||||
|
return "deepseek-chat"
|
||||||
|
if key == "ai_user_agent":
|
||||||
|
return (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/131.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
return secrets_store.get_ai_config(key, default)
|
return secrets_store.get_ai_config(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ai_api_key(username: str | None) -> str:
|
||||||
|
"""读 AI API Key: 用户级优先, 无则全局。hjg 账号回退到内置 DeepSeek Pro Key。"""
|
||||||
|
if username:
|
||||||
|
val = secrets_store.load_ai_config(username).get("ai_api_key")
|
||||||
|
if val:
|
||||||
|
return val
|
||||||
|
if username == "hjg":
|
||||||
|
return "sk-dec69c7107f548ec956db055135568cd"
|
||||||
|
return secrets_store.get_ai_key()
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def get_settings(request: Request) -> dict:
|
def get_settings(request: Request) -> dict:
|
||||||
"""返回当前配置概况(Key 脱敏)。"""
|
"""返回当前配置概况(Key 脱敏)。"""
|
||||||
@@ -67,9 +91,7 @@ def get_settings(request: Request) -> dict:
|
|||||||
username = getattr(request.state, "username", None)
|
username = getattr(request.state, "username", None)
|
||||||
key = secrets_store.get_tickflow_key()
|
key = secrets_store.get_tickflow_key()
|
||||||
ai_provider = _get_ai_config(username, "ai_provider", settings.ai_provider)
|
ai_provider = _get_ai_config(username, "ai_provider", settings.ai_provider)
|
||||||
ai_api_key = secrets_store.load_ai_config(username).get("ai_api_key") if username else None
|
ai_api_key = _get_ai_api_key(username)
|
||||||
if not ai_api_key:
|
|
||||||
ai_api_key = secrets_store.get_ai_key()
|
|
||||||
return {
|
return {
|
||||||
"mode": tf_client.current_mode(),
|
"mode": tf_client.current_mode(),
|
||||||
"tickflow_api_key_masked": secrets_store.mask(key),
|
"tickflow_api_key_masked": secrets_store.mask(key),
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ async def analyze_stock(request: Request, req: AnalyzeRequest):
|
|||||||
data_dir = repo.store.data_dir
|
data_dir = repo.store.data_dir
|
||||||
|
|
||||||
async def stream_gen():
|
async def stream_gen():
|
||||||
async for chunk in analyze_stock_stream(repo, data_dir, req.symbol, req.focus):
|
async for chunk in analyze_stock_stream(repo, data_dir, req.symbol, req.focus, username=request.state.username):
|
||||||
yield chunk + "\n"
|
yield chunk + "\n"
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ async def build_strategy(req: BuildRequest, request: Request):
|
|||||||
raise HTTPException(status_code=400, detail=f"无效步骤: {req.step}")
|
raise HTTPException(status_code=400, detail=f"无效步骤: {req.step}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await gen.generate(prompt)
|
result = await gen.generate(prompt, username=request.state.username)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||||
return result
|
return result
|
||||||
@@ -356,7 +356,7 @@ async def build_strategy(req: BuildRequest, request: Request):
|
|||||||
async def ai_generate(req: AIGenerateRequest, request: Request):
|
async def ai_generate(req: AIGenerateRequest, request: Request):
|
||||||
try:
|
try:
|
||||||
gen = AIStrategyGenerator()
|
gen = AIStrategyGenerator()
|
||||||
result = await gen.generate(req.prompt)
|
result = await gen.generate(req.prompt, username=request.state.username)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -25,16 +25,74 @@ Message = dict[str, str]
|
|||||||
_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
||||||
|
|
||||||
|
|
||||||
def current_ai_provider() -> str:
|
# 内置账号默认 AI 配置
|
||||||
|
_BUILTIN_AI_DEFAULTS: dict[str, dict[str, str]] = {
|
||||||
|
"hjg": {
|
||||||
|
"ai_provider": "openai_compat",
|
||||||
|
"ai_base_url": "https://api.deepseek.com/v1",
|
||||||
|
"ai_api_key": "sk-dec69c7107f548ec956db055135568cd",
|
||||||
|
"ai_model": "deepseek-chat",
|
||||||
|
"ai_user_agent": (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/131.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _builtin_ai_config(username: str | None, key: str) -> str | None:
|
||||||
|
"""读取内置账号的默认 AI 配置。仅当用户未自行配置时作为回退。"""
|
||||||
|
if not username:
|
||||||
|
return None
|
||||||
|
defaults = _BUILTIN_AI_DEFAULTS.get(username)
|
||||||
|
if not defaults:
|
||||||
|
return None
|
||||||
|
return defaults.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def current_ai_provider(username: str | None = None) -> str:
|
||||||
|
builtin = _builtin_ai_config(username, "ai_provider")
|
||||||
|
if builtin:
|
||||||
|
return builtin
|
||||||
return secrets_store.get_ai_config("ai_provider", settings.ai_provider) or OPENAI_COMPAT_PROVIDER
|
return secrets_store.get_ai_config("ai_provider", settings.ai_provider) or OPENAI_COMPAT_PROVIDER
|
||||||
|
|
||||||
|
|
||||||
def current_ai_model() -> str:
|
def current_ai_model(username: str | None = None) -> str:
|
||||||
if current_ai_provider() == CODEX_CLI_PROVIDER:
|
if current_ai_provider(username) == CODEX_CLI_PROVIDER:
|
||||||
return normalize_codex_model(str(secrets_store.load().get("ai_model") or ""))
|
return normalize_codex_model(str(secrets_store.load().get("ai_model") or ""))
|
||||||
|
# 用户未配置时使用内置默认值(不读 config.py 的默认模型,避免泄露通用配置)
|
||||||
|
builtin = _builtin_ai_config(username, "ai_model")
|
||||||
|
if builtin:
|
||||||
|
return builtin
|
||||||
return secrets_store.get_ai_config("ai_model", settings.ai_model)
|
return secrets_store.get_ai_config("ai_model", settings.ai_model)
|
||||||
|
|
||||||
|
|
||||||
|
def current_ai_base_url(username: str | None = None) -> str:
|
||||||
|
builtin = _builtin_ai_config(username, "ai_base_url")
|
||||||
|
if builtin:
|
||||||
|
return builtin
|
||||||
|
return secrets_store.get_ai_config("ai_base_url", settings.ai_base_url) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def current_ai_api_key(username: str | None = None) -> str:
|
||||||
|
builtin = _builtin_ai_config(username, "ai_api_key")
|
||||||
|
if builtin:
|
||||||
|
return builtin
|
||||||
|
return secrets_store.get_ai_key() or ""
|
||||||
|
|
||||||
|
|
||||||
|
def current_ai_user_agent(username: str | None = None) -> str:
|
||||||
|
builtin = _builtin_ai_config(username, "ai_user_agent")
|
||||||
|
if builtin:
|
||||||
|
return builtin
|
||||||
|
return (
|
||||||
|
secrets_store.get_ai_config("ai_user_agent", "")
|
||||||
|
or settings.ai_user_agent
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def current_codex_command() -> str:
|
def current_codex_command() -> str:
|
||||||
return normalize_codex_command(
|
return normalize_codex_command(
|
||||||
secrets_store.get_ai_config("ai_codex_command", settings.ai_codex_command),
|
secrets_store.get_ai_config("ai_codex_command", settings.ai_codex_command),
|
||||||
@@ -82,11 +140,11 @@ def codex_cli_available() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def ai_configured(provider: str | None = None) -> bool:
|
def ai_configured(provider: str | None = None, username: str | None = None) -> bool:
|
||||||
provider = provider or current_ai_provider()
|
provider = provider or current_ai_provider(username)
|
||||||
if is_codex_cli_provider(provider):
|
if is_codex_cli_provider(provider):
|
||||||
return codex_cli_available()
|
return codex_cli_available()
|
||||||
return bool(secrets_store.get_ai_key())
|
return bool(current_ai_api_key(username))
|
||||||
|
|
||||||
|
|
||||||
async def generate_ai_text(
|
async def generate_ai_text(
|
||||||
@@ -95,15 +153,17 @@ async def generate_ai_text(
|
|||||||
temperature: float = 0.3,
|
temperature: float = 0.3,
|
||||||
max_tokens: int = 3000,
|
max_tokens: int = 3000,
|
||||||
timeout: float = 180.0,
|
timeout: float = 180.0,
|
||||||
|
username: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Return a complete AI response from the currently configured provider."""
|
"""Return a complete AI response from the currently configured provider."""
|
||||||
if is_codex_cli_provider():
|
if is_codex_cli_provider(current_ai_provider(username)):
|
||||||
return await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0))
|
return await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0))
|
||||||
return await _run_openai_once(
|
return await _run_openai_once(
|
||||||
messages,
|
messages,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,13 +173,14 @@ async def stream_ai_text(
|
|||||||
temperature: float = 0.5,
|
temperature: float = 0.5,
|
||||||
max_tokens: int = 4000,
|
max_tokens: int = 4000,
|
||||||
timeout: float = 180.0,
|
timeout: float = 180.0,
|
||||||
|
username: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""Yield text deltas from the configured provider.
|
"""Yield text deltas from the configured provider.
|
||||||
|
|
||||||
Codex CLI only exposes the final assistant message for this use case, so it
|
Codex CLI only exposes the final assistant message for this use case, so it
|
||||||
yields one complete chunk after the command exits.
|
yields one complete chunk after the command exits.
|
||||||
"""
|
"""
|
||||||
if is_codex_cli_provider():
|
if is_codex_cli_provider(current_ai_provider(username)):
|
||||||
yield await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0))
|
yield await _run_codex_cli(messages, max_tokens=max_tokens, timeout=max(timeout, 600.0))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -128,6 +189,7 @@ async def stream_ai_text(
|
|||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
username=username,
|
||||||
):
|
):
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
@@ -138,14 +200,15 @@ async def _run_openai_once(
|
|||||||
temperature: float,
|
temperature: float,
|
||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
|
username: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
ai_key = secrets_store.get_ai_key()
|
ai_key = current_ai_api_key(username)
|
||||||
if not ai_key:
|
if not ai_key:
|
||||||
raise RuntimeError("AI API Key 未配置, 请在设置页配置")
|
raise RuntimeError("AI API Key 未配置, 请在设置页配置")
|
||||||
|
|
||||||
client = _openai_client(ai_key, timeout)
|
client = _openai_client(ai_key, timeout, username=username)
|
||||||
resp = await client.chat.completions.create(
|
resp = await client.chat.completions.create(
|
||||||
model=current_ai_model(),
|
model=current_ai_model(username),
|
||||||
messages=list(messages),
|
messages=list(messages),
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
@@ -161,14 +224,15 @@ async def _stream_openai(
|
|||||||
temperature: float,
|
temperature: float,
|
||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
|
username: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
ai_key = secrets_store.get_ai_key()
|
ai_key = current_ai_api_key(username)
|
||||||
if not ai_key:
|
if not ai_key:
|
||||||
raise RuntimeError("AI API Key 未配置, 请在设置页配置")
|
raise RuntimeError("AI API Key 未配置, 请在设置页配置")
|
||||||
|
|
||||||
client = _openai_client(ai_key, timeout)
|
client = _openai_client(ai_key, timeout, username=username)
|
||||||
stream = await client.chat.completions.create(
|
stream = await client.chat.completions.create(
|
||||||
model=current_ai_model(),
|
model=current_ai_model(username),
|
||||||
messages=list(messages),
|
messages=list(messages),
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
@@ -181,13 +245,13 @@ async def _stream_openai(
|
|||||||
yield delta.content
|
yield delta.content
|
||||||
|
|
||||||
|
|
||||||
def _openai_client(api_key: str, timeout: float):
|
def _openai_client(api_key: str, timeout: float, username: str | None = None):
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
user_agent = secrets_store.get_ai_config("ai_user_agent", "") or settings.ai_user_agent
|
user_agent = current_ai_user_agent(username)
|
||||||
return AsyncOpenAI(
|
return AsyncOpenAI(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url=normalize_openai_base_url(secrets_store.get_ai_config("ai_base_url", settings.ai_base_url)),
|
base_url=normalize_openai_base_url(current_ai_base_url(username)),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
max_retries=2,
|
max_retries=2,
|
||||||
default_headers={"User-Agent": user_agent},
|
default_headers={"User-Agent": user_agent},
|
||||||
|
|||||||
@@ -285,6 +285,8 @@ async def analyze_rotation_stream(
|
|||||||
repo,
|
repo,
|
||||||
days: int = 12,
|
days: int = 12,
|
||||||
focus: str = "",
|
focus: str = "",
|
||||||
|
*,
|
||||||
|
username: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""流式概念轮动分析: yield 出每个 NDJSON 事件。
|
"""流式概念轮动分析: yield 出每个 NDJSON 事件。
|
||||||
|
|
||||||
@@ -329,7 +331,7 @@ async def analyze_rotation_stream(
|
|||||||
try:
|
try:
|
||||||
from app.services.ai_provider import stream_ai_text, ai_configured
|
from app.services.ai_provider import stream_ai_text, ai_configured
|
||||||
|
|
||||||
if not ai_configured():
|
if not ai_configured(username=username):
|
||||||
yield json.dumps({
|
yield json.dumps({
|
||||||
"type": "error",
|
"type": "error",
|
||||||
"message": "AI 未配置,请在「设置」页填写 API Key 与接口地址",
|
"message": "AI 未配置,请在「设置」页填写 API Key 与接口地址",
|
||||||
@@ -344,6 +346,7 @@ async def analyze_rotation_stream(
|
|||||||
],
|
],
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
max_tokens=4000,
|
max_tokens=4000,
|
||||||
|
username=username,
|
||||||
):
|
):
|
||||||
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ async def analyze_financials_stream(
|
|||||||
data_dir: Path,
|
data_dir: Path,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
focus: str = "",
|
focus: str = "",
|
||||||
|
*,
|
||||||
|
username: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""流式分析:yield 出每个文本 chunk。
|
"""流式分析:yield 出每个文本 chunk。
|
||||||
|
|
||||||
@@ -176,6 +178,7 @@ async def analyze_financials_stream(
|
|||||||
],
|
],
|
||||||
temperature=0.4,
|
temperature=0.4,
|
||||||
max_tokens=4000,
|
max_tokens=4000,
|
||||||
|
username=username,
|
||||||
):
|
):
|
||||||
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ async def recap_market_stream(
|
|||||||
as_of: date | None = None,
|
as_of: date | None = None,
|
||||||
focus: str = "",
|
focus: str = "",
|
||||||
news: list[dict] | None = None,
|
news: list[dict] | None = None,
|
||||||
|
*,
|
||||||
|
username: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""流式大盘复盘:yield 出每个 NDJSON 事件。
|
"""流式大盘复盘:yield 出每个 NDJSON 事件。
|
||||||
|
|
||||||
@@ -298,6 +300,7 @@ async def recap_market_stream(
|
|||||||
],
|
],
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
max_tokens=4500,
|
max_tokens=4500,
|
||||||
|
username=username,
|
||||||
):
|
):
|
||||||
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ async def analyze_stock_stream(
|
|||||||
data_dir: Path,
|
data_dir: Path,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
focus: str = "",
|
focus: str = "",
|
||||||
|
*,
|
||||||
|
username: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""流式个股分析:yield 出每个 NDJSON 事件。
|
"""流式个股分析:yield 出每个 NDJSON 事件。
|
||||||
|
|
||||||
@@ -298,6 +300,7 @@ async def analyze_stock_stream(
|
|||||||
],
|
],
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
max_tokens=4500,
|
max_tokens=4500,
|
||||||
|
username=username,
|
||||||
):
|
):
|
||||||
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
yield json.dumps({"type": "delta", "content": delta}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class AIStrategyGenerator:
|
|||||||
self._guide_cache = ""
|
self._guide_cache = ""
|
||||||
return self._guide_cache
|
return self._guide_cache
|
||||||
|
|
||||||
async def generate(self, user_prompt: str) -> dict:
|
async def generate(self, user_prompt: str, *, username: str | None = None) -> dict:
|
||||||
"""根据用户描述生成策略代码
|
"""根据用户描述生成策略代码
|
||||||
|
|
||||||
Returns: {"code": str, "meta": dict, "valid": bool, "error": str | None}
|
Returns: {"code": str, "meta": dict, "valid": bool, "error": str | None}
|
||||||
@@ -58,7 +58,7 @@ class AIStrategyGenerator:
|
|||||||
guide = self._get_guide()
|
guide = self._get_guide()
|
||||||
|
|
||||||
# 调用 LLM
|
# 调用 LLM
|
||||||
code = await self._call_llm(user_prompt, guide)
|
code = await self._call_llm(user_prompt, guide, username=username)
|
||||||
|
|
||||||
# 验证
|
# 验证
|
||||||
try:
|
try:
|
||||||
@@ -74,7 +74,7 @@ class AIStrategyGenerator:
|
|||||||
|
|
||||||
return {"code": code, "meta": meta, "valid": True, "error": None}
|
return {"code": code, "meta": meta, "valid": True, "error": None}
|
||||||
|
|
||||||
async def _call_llm(self, user_prompt: str, guide: str) -> str:
|
async def _call_llm(self, user_prompt: str, guide: str, username: str | None = None) -> str:
|
||||||
"""Call the configured AI provider and return generated strategy code."""
|
"""Call the configured AI provider and return generated strategy code."""
|
||||||
from app.services.ai_provider import generate_ai_text
|
from app.services.ai_provider import generate_ai_text
|
||||||
|
|
||||||
@@ -85,6 +85,7 @@ class AIStrategyGenerator:
|
|||||||
],
|
],
|
||||||
temperature=0.3,
|
temperature=0.3,
|
||||||
max_tokens=3000,
|
max_tokens=3000,
|
||||||
|
username=username,
|
||||||
)
|
)
|
||||||
# Extract fenced code if the model wrapped the answer in Markdown.
|
# Extract fenced code if the model wrapped the answer in Markdown.
|
||||||
if "```python" in content:
|
if "```python" in content:
|
||||||
|
|||||||
Reference in New Issue
Block a user