Merge branch 'dhk-1' into 'master'

Dhk 1

See merge request knowledge-base-bj/deerflow-server!20
This commit is contained in:
董宏凯 committed 2026-05-18 14:53:35 +00:00
commit 613e22d6ca
13 files changed
+259 -76

No files matched your search

@@ -4,6 +4,13 @@ import { ChevronsDown, Play, Plus, RotateCcw, Settings2, Square } from 'lucide-r
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { useArticleTypes } from '../../hooks/useArticleTypes';
import { useCanvasGraphContext } from '../../contexts/CanvasGraphContext';
import { useAIWriting } from '../../hooks/useAIWriting';
@@ -40,9 +47,13 @@ export function AIWritingPanel() {
const { setArtifact } = useCanvasGraphContext();
const { state, startWriting, submitIntervention, pauseWriting, resumeWriting, reset } = useAIWriting();
const { sessions, loading: sessionsLoading, refresh, rename, remove } = useAIWritingSessions();
const { articleTypes, refresh: refreshArticleTypes } = useArticleTypes();
useEffect(() => { refreshArticleTypes(); }, [refreshArticleTypes]);
// History view state — null means normal writing mode
const [historySession, setHistorySession] = useState<AIWritingSessionSummary | null>(null);
const [reqDialogOpen, setReqDialogOpen] = useState(false);
const isIdle = state.status === 'idle';
const isDone = state.status === 'done';
@@ -203,7 +214,15 @@ export function AIWritingPanel() {
<div className="flex flex-col h-full overflow-hidden bg-background">
{/* 头部 */}
<div className="flex items-center justify-between px-4 py-2.5 border-b shrink-0">
<span className="text-sm font-semibold shrink-0">AI 智能写作</span>
<span
className={cn(
'text-sm font-semibold shrink-0',
!isIdle && 'cursor-pointer hover:text-primary transition-colors',
)}
onClick={() => { if (!isIdle && state.request) setReqDialogOpen(true); }}
>
AI 智能写作
</span>
<div className="flex items-center gap-1.5">
<Button
variant="ghost"
@@ -383,6 +402,47 @@ export function AIWritingPanel() {
)}
</div>
)}
{/* 写作参数查看弹窗 */}
{state.request && (
<Dialog open={reqDialogOpen} onOpenChange={setReqDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>本次写作参数</DialogTitle>
</DialogHeader>
<div className="space-y-3 py-1 text-sm">
<ReqRow label="写作意图">
<p className="text-muted-foreground whitespace-pre-wrap leading-relaxed">{state.request.userIntent}</p>
</ReqRow>
<ReqRow label="文章类型">
<span className="text-muted-foreground">
{articleTypes.find((t) => t.key === state.request!.articleType)?.label ?? state.request.articleType}
</span>
</ReqRow>
<ReqRow label="目标读者">
<span className="text-muted-foreground">{state.request.targetAudience}</span>
</ReqRow>
<ReqRow label="目标字数">
<span className="text-muted-foreground">{state.request.wordCountTarget} 字</span>
</ReqRow>
{state.request.modelName && (
<ReqRow label="写作模型">
<span className="text-muted-foreground">{state.request.modelName}</span>
</ReqRow>
)}
</div>
</DialogContent>
</Dialog>
)}
</div>
);
}
function ReqRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="space-y-0.5">
<p className="text-xs font-medium text-foreground/60">{label}</p>
{children}
</div>
);
}
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useRef, useState } from 'react';
import {
PauseCircle, CheckCircle2, RotateCcw, Search,
RefreshCw, PenLine, ArrowRight, ShieldAlert, ClipboardEdit,
RefreshCw, PenLine, ArrowRight, ShieldAlert, ClipboardEdit, SendHorizonal,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import type {
@@ -222,9 +222,9 @@ interface OutlineConfirmCardProps {
function OutlineConfirmCard({ outline: initialOutline, onSubmit, disabled }: OutlineConfirmCardProps) {
const [editedOutline, setEditedOutline] = useState<AIOutline | null>(initialOutline);
const [showReplan, setShowReplan] = useState(false);
const [feedback, setFeedback] = useState('');
const [submitting, setSubmitting] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
async function handleConfirm() {
setSubmitting(true);
@@ -236,11 +236,28 @@ function OutlineConfirmCard({ outline: initialOutline, onSubmit, disabled }: Out
}
async function handleReplan() {
const text = feedback.trim();
if (!text) return;
setSubmitting(true);
await onSubmit({ action: 're_outline', outlineFeedback: feedback.trim() });
await onSubmit({ action: 're_outline', outlineFeedback: text });
setSubmitting(false);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleReplan();
}
}
// 自动调整高度
function handleInput(e: React.ChangeEvent<HTMLTextAreaElement>) {
setFeedback(e.target.value);
const el = e.target;
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
}
return (
<div className="rounded-md border bg-background space-y-3 p-3">
<div className="flex items-start gap-2">
@@ -248,7 +265,7 @@ function OutlineConfirmCard({ outline: initialOutline, onSubmit, disabled }: Out
<div>
<p className="text-xs font-medium">请确认大纲后继续写作</p>
<p className="text-xs text-muted-foreground mt-0.5">
可直接点击章节标题或要点进行编辑,或要求重新规划大纲。
可直接点击章节标题或要点进行编辑,或在下方输入意见让 AI 重新规划。
</p>
</div>
</div>
@@ -257,40 +274,37 @@ function OutlineConfirmCard({ outline: initialOutline, onSubmit, disabled }: Out
? <OutlineCard outline={editedOutline} onChange={setEditedOutline} />
: <p className="text-xs text-muted-foreground text-center py-2">大纲加载中…</p>}
{showReplan && (
<div className="space-y-1.5">
<label className="text-xs text-muted-foreground">重新规划意见(可选)</label>
<textarea
rows={2} value={feedback} onChange={(e) => setFeedback(e.target.value)}
placeholder="描述大纲调整方向,例如:增加案例分析章节…"
className="w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs resize-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
</div>
)}
<div className="flex gap-2 pt-1">
<Button
type="button" size="sm" className="flex-1 h-8 text-xs gap-1.5"
onClick={handleConfirm} disabled={disabled || submitting || !editedOutline}
{/* 重新规划输入框 */}
<div className="flex items-end gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 focus-within:ring-1 focus-within:ring-ring">
<textarea
ref={textareaRef}
rows={1}
value={feedback}
onChange={handleInput}
onKeyDown={handleKeyDown}
placeholder="输入修改意见后按 Enter 重新规划大纲,Shift+Enter 换行…"
disabled={disabled || submitting}
className="flex-1 resize-none overflow-hidden bg-transparent text-xs placeholder:text-muted-foreground focus:outline-none disabled:opacity-50 leading-5 py-0.5 min-h-[20px] max-h-32"
/>
<button
type="button"
onClick={handleReplan}
disabled={disabled || submitting || !feedback.trim()}
className="shrink-0 mb-0.5 p-1 rounded text-muted-foreground hover:text-primary hover:bg-primary/8 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
title="发送(Enter)"
>
<CheckCircle2 className="size-3.5" />确认大纲,开始写作
</Button>
{showReplan ? (
<Button
type="button" variant="outline" size="sm" className="h-8 text-xs gap-1.5 shrink-0"
onClick={handleReplan} disabled={disabled || submitting}
>
<RefreshCw className="size-3.5" />提交重规划
</Button>
) : (
<Button
type="button" variant="outline" size="sm" className="h-8 text-xs gap-1.5 shrink-0"
onClick={() => setShowReplan(true)} disabled={disabled || submitting}
>
<RotateCcw className="size-3.5" />重新规划…
</Button>
)}
{submitting
? <RefreshCw className="size-3.5 animate-spin" />
: <SendHorizonal className="size-3.5" />}
</button>
</div>
<Button
type="button" size="sm" className="w-full h-8 text-xs gap-1.5"
onClick={handleConfirm} disabled={disabled || submitting || !editedOutline}
>
<CheckCircle2 className="size-3.5" />确认大纲,开始写作
</Button>
</div>
);
}
@@ -21,6 +21,7 @@ import type {
const INITIAL_STATE: AIWritingState = {
sessionId: null,
status: 'idle',
request: null,
isPaused: false,
progressEvents: [],
materials: [],
@@ -269,7 +270,7 @@ export function useAIWriting() {
abortRef.current?.abort();
abortRef.current = new AbortController();
setState({ ...INITIAL_STATE, status: 'researching' });
setState({ ...INITIAL_STATE, status: 'researching', request });
try {
const sessionId = await startWritingSession(request);
@@ -95,11 +95,9 @@ export default function ArticleTypesConfigPage() {
}
async function handleSave() {
const trimmedKey = form.key.trim();
const trimmedLabel = form.label.trim();
const wordCount = parseInt(form.default_word_count, 10);
if (!trimmedKey) { setFormError('标识不能为空'); return; }
if (!trimmedLabel) { setFormError('名称不能为空'); return; }
if (isNaN(wordCount) || wordCount < 100) { setFormError('默认字数不能小于 100'); return; }
@@ -116,8 +114,9 @@ export default function ArticleTypesConfigPage() {
};
await update(editingId, req);
} else {
const autoKey = 't_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
const req: CreateArticleTypeRequest = {
key: trimmedKey,
key: autoKey,
label: trimmedLabel,
description: form.description.trim() || undefined,
prompt_hint: form.prompt_hint.trim() || undefined,
@@ -211,19 +210,6 @@ export default function ArticleTypesConfigPage() {
</DialogHeader>
<div className="space-y-3 py-1">
<FormField label="标识 (key)" required>
<input
className={INPUT_CLASS}
placeholder="如 news、analysis(传给 AI 的唯一标识,仅限字母数字下划线)"
value={form.key}
onChange={(e) => setField('key', e.target.value)}
disabled={!!editingId}
/>
{editingId && (
<p className="text-[11px] text-muted-foreground mt-0.5">标识创建后不可修改</p>
)}
</FormField>
<FormField label="名称" required>
<input
className={INPUT_CLASS}
@@ -355,9 +341,6 @@ function ArticleTypeCard({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold">{type.label}</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono bg-muted text-muted-foreground border">
{type.key}
</span>
</div>
{type.description && (
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{type.description}</p>
@@ -189,6 +189,7 @@ export interface CompletedIntervention {
export interface AIWritingState {
sessionId: string | null;
status: WritingStatus;
request: AIWritingRequest | null;
isPaused: boolean;
progressEvents: ProgressEvent[];
materials: AIMaterial[];
@@ -15,10 +15,15 @@ from langchain_core.runnables import RunnableConfig
logger = logging.getLogger(__name__)
def get_model(config: RunnableConfig):
def get_model(config: RunnableConfig, model_name: str | None = None):
"""创建对话模型。
model_name 显式传入时优先使用(如素材收集专家的检索词/排序专用快模型),
否则回退到 configurable.model_name(用户在写作表单里选的主模型)。
"""
from deerflow.models import create_chat_model
model_name = config.get("configurable", {}).get("model_name", "") or None
return create_chat_model(name=model_name)
name = model_name or config.get("configurable", {}).get("model_name", "") or None
return create_chat_model(name=name)
# 弱模型常把结构性标点写成中文全角符号,仅在常规解析失败后作为修复手段
@@ -220,11 +225,16 @@ class ThinkTagParser:
return 0
async def llm_json(system: str, user: str, fallback: dict, config: RunnableConfig) -> dict:
"""调用 LLM,解析 JSON 输出,失败用 fallback。"""
model = get_model(config)
async def llm_json(system: str, user: str, fallback: dict, config: RunnableConfig, model_name: str | None = None) -> dict:
"""调用 LLM,解析 JSON 输出,失败用 fallback。
model_name 可覆盖默认模型(如检索词/排序使用配置的专用快模型)。
"""
messages = [SystemMessage(content=system), HumanMessage(content=user)]
try:
# get_model 放进 try:模型名在 config.yaml 找不到时 create_chat_model 会抛
# ValueError,若留在 try 外会逐层上抛使整个图硬崩溃。
model = get_model(config, model_name=model_name)
resp = await model.ainvoke(messages)
return parse_json(_content_to_text(resp.content), fallback)
except Exception as e:
@@ -89,7 +89,19 @@ async def editor_node(state: AIWritingState, config: RunnableConfig) -> dict:
("language", LANGUAGE_REVIEW_USER.format(article=article)),
]
model = get_model(config)
# 模型创建守卫:模型名配错时 create_chat_model 抛 ValueError,
# 不拦截会硬崩溃整个图;此处转成可恢复的 status:error。
try:
model = get_model(config)
except Exception as e:
logger.error("editor model creation failed: %s", e, exc_info=True)
return {
"status": "error",
"progress_events": [{
"type": "error", "agent_name": "编辑",
"title": "模型错误", "detail": str(e), "message": "审核失败",
}],
}
results: dict[str, dict] = {}
review_errors: list[str] = [] # 分项审查的模型异常,供完成后非致命提示
@@ -1,6 +1,8 @@
"""意图解析节点:将用户自然语言意图结构化为写作配置。"""
from __future__ import annotations
import re
from langchain_core.runnables import RunnableConfig
from ..state import AIWritingState
@@ -43,6 +45,33 @@ def _build_type_options(types: list[dict]) -> str:
return " / ".join(f'{t["key"]}({t["label"]})' for t in types)
def _coerce_word_count(raw) -> int | None:
"""稳健地把模型返回的字数字段转成正整数;无法解析时返回 None。
弱模型(尤其老模型被陌生词带偏时)常返回「约800字」「两千」「800.0」
这类非纯数字形式,直接 int() 会抛 ValueError/TypeError 使整个图崩溃。
此处用「提取首个数字串」策略容错,解析不出就返回 None 让调用方保留默认值。
"""
if raw is None:
return None
if isinstance(raw, bool): # bool 是 int 子类,需先排除
return None
if isinstance(raw, (int, float)):
value = int(raw)
return value if value > 0 else None
if not isinstance(raw, str):
# dict / list 等结构化类型:str() 化后可能误含数字,直接判为无法解析
return None
m = re.search(r"\d[\d,]*", raw)
if not m:
return None
try:
value = int(m.group().replace(",", ""))
except ValueError:
return None
return value if value > 0 else None
async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> dict:
# 从 configurable 读取动态类型列表,未配置时退回默认值
configurable = (config or {}).get("configurable", {})
@@ -73,10 +102,19 @@ async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> d
config=config,
)
updates: dict = {
"status": "researching",
"progress_events": [{"type": "step_done", "agent_name": "系统", "message": "意图解析完成"}],
}
# 意图解析模型调用失败时以非致命 notice 提示用户(llm_json 已用 fallback 兜底,
# 流程仍可继续,但用户需知道结构化配置可能不准)。
progress_events: list[dict] = []
if result.get("_llm_error"):
progress_events.append({
"type": "notice",
"agent_name": "系统",
"title": "模型错误",
"detail": f"意图解析失败,已按默认配置兜底。原因:{result['_llm_error']}",
})
progress_events.append({"type": "step_done", "agent_name": "系统", "message": "意图解析完成"})
updates: dict = {"status": "researching", "progress_events": progress_events}
# 校验 LLM 返回的 article_type 是否在有效范围内,不合法时回退到默认值
raw_type = result.get("article_type", "")
@@ -84,8 +122,9 @@ async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> d
if result.get("target_audience"):
updates["target_audience"] = result["target_audience"]
if result.get("word_count_target"):
updates["word_count_target"] = int(result["word_count_target"])
word_count = _coerce_word_count(result.get("word_count_target"))
if word_count is not None:
updates["word_count_target"] = word_count
if result.get("refined_intent"):
updates["user_intent"] = result["refined_intent"]
@@ -40,6 +40,12 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
sse_queue = cfg.get("sse_queue")
sse_session_id: str = cfg.get("sse_session_id", "")
# 素材收集专家专用模型:检索词生成 / 素材排序 可在 config.yaml 的
# ai_writing 段单独配置(keyword_model / rank_model),留空则回退写作主模型。
ai_writing_config = cfg.get("ai_writing_config", {}) or {}
keyword_model: str | None = ai_writing_config.get("keyword_model") or None
rank_model: str | None = ai_writing_config.get("rank_model") or None
async def _emit(message: str) -> None:
if sse_queue is None:
return
@@ -59,13 +65,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
),
fallback={"keywords": [intent]},
config=config,
model_name=keyword_model,
)
keywords: list[str] = kw_result.get("keywords", [intent])
if extra_keywords:
keywords = list(dict.fromkeys(keywords + extra_keywords))
# Step 2: 并发搜索
ai_writing_config = config.get("configurable", {}).get("ai_writing_config", {})
provider = get_search_provider(ai_writing_config)
search_keywords = keywords[:6]
@@ -136,6 +142,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
RANK_MATERIALS_USER.format(user_intent=intent, materials_list=materials_list),
fallback={"ranked_ids": [m["id"] for m in raw_materials[:_MAX_MATERIALS]], "summary": f"共搜索到{len(raw_materials)}条素材"},
config=config,
model_name=rank_model,
)
rank_error = rank_result.get("_llm_error", "")
ranked_ids: list[str] = rank_result.get("ranked_ids", [m["id"] for m in raw_materials])
@@ -73,7 +73,19 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
def _section_title(idx: int, sec: dict) -> str:
return sec.get("section_title") or sec.get("sectionTitle") or f"章节{idx + 1}"
model = get_model(config)
# 模型创建守卫:模型名配错时 create_chat_model 抛 ValueError,
# 不拦截会硬崩溃整个图;此处转成可恢复的 status:error。
try:
model = get_model(config)
except Exception as e:
logger.error("draft model creation failed: %s", e, exc_info=True)
return {
"status": "error",
"progress_events": [{
"type": "error", "agent_name": "作家",
"title": "模型错误", "detail": str(e), "message": "草稿生成失败",
}],
}
section_titles: list[str] = []
section_bufs: list[str] = [] # 各章节正文(不含 ## 标题),供扩写兜底逐节替换
section_errors: list[str] = [] # 分节写作的模型异常,供完成后非致命提示
@@ -62,11 +62,13 @@ async def writer_outline_node(state: AIWritingState, config: RunnableConfig) ->
sse_queue: asyncio.Queue | None = cfg.get("sse_queue")
session_id: str = cfg.get("sse_session_id", "")
model = get_model(config)
full_text = "" # 只累积正文(非思考)用于 JSON 解析
parser = ThinkTagParser()
try:
# get_model 放进 try:模型名配错时 create_chat_model 抛 ValueError,
# 留在 try 外会硬崩溃;放进来则统一走下方的 status:error 兜底。
model = get_model(config)
async for chunk in model.astream([
SystemMessage(content=OUTLINE_SYSTEM),
HumanMessage(content=user_prompt),
@@ -6,6 +6,11 @@ EXTRACT_KEYWORDS_SYSTEM = """你是一名专业情报研究员(素材收集专
1. 从写作意图中提取 3-6 个搜索关键词
你不能写文章,不能评价内容好坏,只做关键词提取。
【严禁思考】
- 这是一个简单的结构化抽取任务,禁止任何思考、分析、推理或规划过程。
- 禁止输出 <think></think> 标签及其中的任何内容,禁止输出"让我想想""首先"之类的过渡语。
- 收到输入后必须立即、直接输出最终 JSON,不得有任何前置内容。
【输出要求】
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
- 必须使用英文双引号 ",不要用中文引号 “”。
@@ -31,6 +36,11 @@ RANK_MATERIALS_SYSTEM = """你是一名专业情报研究员(素材收集专
1. 从提供的素材列表中,选出与写作意图最相关的素材ID,按相关度从高到低排序
你不能写文章,只做素材筛选排序。
【严禁思考】
- 这是一个简单的筛选排序任务,禁止任何思考、分析、推理或规划过程。
- 禁止输出 <think></think> 标签及其中的任何内容,禁止输出"让我想想""首先"之类的过渡语。
- 收到输入后必须立即、直接输出最终 JSON,不得有任何前置内容。
【输出要求】
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
- 必须使用英文双引号 "。
+34 -2
View File
@@ -222,6 +222,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
name = "asyncmy"
version = "0.2.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/3c/d8297584c40f3d1af55365026bcdca7844ecfea1d917ad19df48f8331a26/asyncmy-0.2.11.tar.gz", hash = "sha256:c3d65d959dde62c911e39ecd1ad0f1339a5e6929fc411d48cfc2f82846190bf4", size = 62865, upload-time = "2026-01-15T11:32:30.368Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/93/3b4c7f9b35a27e80bd2f305c4c8d7ae56b6dd40d616d34dd4dbb818c90c9/asyncmy-0.2.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:914dddd2ba884822a304297f6ca96026548a100339a4e0ca5c427f6cfa3e4b62", size = 1731817, upload-time = "2026-01-15T11:31:55.486Z" },
{ url = "https://files.pythonhosted.org/packages/7b/65/c70b2b8d014b21504de7e2027e2456f7774cec855766ec1808da47d70b24/asyncmy-0.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ea7ca4bcf7747b1fd4e80f16aa461972e0931cecd3bc43a3ba8e14a5d368a98", size = 1712017, upload-time = "2026-01-15T11:31:56.795Z" },
{ url = "https://files.pythonhosted.org/packages/65/a9/f326999a1ffacc7738376fa68c7ede164db9e5520bb4dbd35f1fdd5704dd/asyncmy-0.2.11-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b2fd6e04efca56d9176e7b620ee47dd1dd334e2eb03c1ae1580954c2625b99d1", size = 4986605, upload-time = "2026-01-15T11:31:58.462Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f2/634326efc5fca15620eb106194d2287997a31625dc95f1940a3cef2f80a7/asyncmy-0.2.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4df583dd9d09e817c4cc68b706133d5da453faf56da431a24fcdd64b9552e26b", size = 5226256, upload-time = "2026-01-15T11:32:00.095Z" },
{ url = "https://files.pythonhosted.org/packages/35/7f/4ecd2dcee1d13d49a301ab8ee11a33c75ded4b3089bdb7bf5fb385ed162d/asyncmy-0.2.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2d473b683db7fa1acb167a4bc25ea38e398c991427bd8aa708a9b75059a1d33", size = 5045263, upload-time = "2026-01-15T11:32:01.653Z" },
{ url = "https://files.pythonhosted.org/packages/fe/3e/d94fc4a0ca1e2e492982db607c26b48a95ba668e24755f3bf00da68ae9be/asyncmy-0.2.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d03803975e5dfe74ef4af18411da3716681fde78a65a8758237e7e3e4a342ba1", size = 5194397, upload-time = "2026-01-15T11:32:03.068Z" },
{ url = "https://files.pythonhosted.org/packages/1e/06/d1bb47ce9ed32ba02f2ff44118d5eb36702d38f97cd824bd6a51d3decfdf/asyncmy-0.2.11-cp312-cp312-win32.whl", hash = "sha256:564fc38b3a0665663b8b2ec35fc34fd2768688ba5c869a0d8bd1eb57b85351b2", size = 1560945, upload-time = "2026-01-15T11:32:04.543Z" },
{ url = "https://files.pythonhosted.org/packages/74/d8/973c576c84f4b706a45372c959778feca6842033ccbbd26b2bfe344ebc4b/asyncmy-0.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:84e23466602407da7e126fb5f2da2948c69a6f0d40d4ea7771331771e05c1c2e", size = 1637442, upload-time = "2026-01-15T11:32:05.802Z" },
{ url = "https://files.pythonhosted.org/packages/83/9a/b5b77690f7287acb0a284319e85378c6f4063cd3617dd5311e00f332d628/asyncmy-0.2.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a48be02bdae5e5324ac2d142d7afc6dd9c6af546fd892804c9d8e58d8107980", size = 1727740, upload-time = "2026-01-15T11:32:07.443Z" },
{ url = "https://files.pythonhosted.org/packages/10/28/7b168dc84704edb0b60f7906bfba3a451fd90c0cb2443edbb377b1a11d20/asyncmy-0.2.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:babed99ea1cf7edb476dba23c27560b2a042de46e61678c0cfa3bc017e5f49e4", size = 1706138, upload-time = "2026-01-15T11:32:08.898Z" },
{ url = "https://files.pythonhosted.org/packages/ec/27/ac7363e8ab95f2048852851bbbef12d4eee62363d202d7e566291023ece4/asyncmy-0.2.11-cp313-cp313-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:709cc8147edad072b5176d87878a985323c87cc017c460073414f2b7d5ae9d01", size = 4942591, upload-time = "2026-01-15T11:32:10.127Z" },
{ url = "https://files.pythonhosted.org/packages/08/81/092314cc97e3732535804f2d3e1b966daeaa3a33a8e9a686328cf09498ad/asyncmy-0.2.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:373aecf8cd17662c13bab69dc36db7242be8e956242164469b8733886fb2ec0a", size = 5178039, upload-time = "2026-01-15T11:32:12.088Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9b/b884404bac62d9b6efbc9006c4b80ad55e8b0bb6f585b44eee1eceb07b1c/asyncmy-0.2.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e23ea478e6638e479dfab2674d2c39a21160c7d750d5c8cf2a0e205d947a63b7", size = 4987628, upload-time = "2026-01-15T11:32:13.979Z" },
{ url = "https://files.pythonhosted.org/packages/00/65/68e576aecd2a43d383123e3a66339e6a3535495b0e81443e374a3d3c356d/asyncmy-0.2.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:577272e238aff9b985eb880b49b1ba009e1fd1133b754fc71c833ab5bd9561ee", size = 5143375, upload-time = "2026-01-15T11:32:15.38Z" },
{ url = "https://files.pythonhosted.org/packages/4e/e4/cd30ea75ab96e5c6fe0daf6bd1871753fe5a1677515530fa0bc1a807dd6c/asyncmy-0.2.11-cp313-cp313-win32.whl", hash = "sha256:29536a08bf8c96437188ae4080fdd09c5a82cbe93794d0996cd0dd238f632664", size = 1559106, upload-time = "2026-01-15T11:32:16.895Z" },
{ url = "https://files.pythonhosted.org/packages/0c/3e/497e3ac839d7d18e79770b977f90e6f17a87181f95b8aed59359ff4aba0c/asyncmy-0.2.11-cp313-cp313-win_amd64.whl", hash = "sha256:f095af7b980505158609ca0bcdd0d14d1e48893e43fc1856c7cecfd9439f498c", size = 1635619, upload-time = "2026-01-15T11:32:18.241Z" },
]
[[package]]
name = "asyncpg"
version = "0.31.0"
@@ -764,6 +788,9 @@ dependencies = [
]
[package.optional-dependencies]
mysql = [
{ name = "deerflow-harness", extra = ["mysql"] },
]
postgres = [
{ name = "deerflow-harness", extra = ["postgres"] },
]
@@ -781,6 +808,7 @@ requires-dist = [
{ name = "bcrypt", specifier = ">=4.0.0" },
{ name = "croniter", specifier = ">=6.2.2" },
{ name = "deerflow-harness", editable = "packages/harness" },
{ name = "deerflow-harness", extras = ["mysql"], marker = "extra == 'mysql'", editable = "packages/harness" },
{ name = "deerflow-harness", extras = ["postgres"], marker = "extra == 'postgres'", editable = "packages/harness" },
{ name = "dingtalk-stream", specifier = ">=0.24.3" },
{ name = "email-validator", specifier = ">=2.0.0" },
@@ -797,7 +825,7 @@ requires-dist = [
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" },
{ name = "wecom-aibot-python-sdk", specifier = ">=0.1.6" },
]
provides-extras = ["postgres"]
provides-extras = ["postgres", "mysql"]
[package.metadata.requires-dev]
dev = [
@@ -847,6 +875,9 @@ dependencies = [
]
[package.optional-dependencies]
mysql = [
{ name = "asyncmy" },
]
ollama = [
{ name = "langchain-ollama" },
]
@@ -866,6 +897,7 @@ requires-dist = [
{ name = "agent-sandbox", specifier = ">=0.0.19" },
{ name = "aiosqlite", specifier = ">=0.19" },
{ name = "alembic", specifier = ">=1.13" },
{ name = "asyncmy", marker = "extra == 'mysql'", specifier = ">=0.2.10" },
{ name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.29" },
{ name = "ddgs", specifier = ">=9.10.0" },
{ name = "dotenv", specifier = ">=0.9.9" },
@@ -901,7 +933,7 @@ requires-dist = [
{ name = "tavily-python", specifier = ">=0.7.17" },
{ name = "tiktoken", specifier = ">=0.8.0" },
]
provides-extras = ["ollama", "postgres", "pymupdf"]
provides-extras = ["ollama", "postgres", "mysql", "pymupdf"]
[[package]]
name = "defusedxml"