智能选中文字
This commit is contained in:
1 parent
e8a31b5d37
commit
da4a08dfe1
34 files changed
+3208
-541
No files matched your search
@@ -43,6 +43,7 @@ from app.gateway.routers import (
|
||||
thread_runs,
|
||||
threads,
|
||||
uploads,
|
||||
writing,
|
||||
)
|
||||
from deerflow.config import app_config as deerflow_app_config
|
||||
from deerflow.config.app_config import apply_logging_level
|
||||
@@ -671,6 +672,9 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
||||
# Suggestions API is mounted at /api/threads/{thread_id}/suggestions
|
||||
app.include_router(suggestions.router)
|
||||
|
||||
# Writing rewrite API is mounted at /api/writing
|
||||
app.include_router(writing.router)
|
||||
|
||||
# Scheduled task API is mounted at /api/scheduled-tasks
|
||||
app.include_router(scheduled_tasks.router)
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from . import artifacts, assistants_compat, mcp, models, recommended_questions, skills, suggestions, thread_runs, threads, uploads
|
||||
from . import artifacts, assistants_compat, mcp, models, recommended_questions, skills, suggestions, thread_runs, threads, uploads, writing
|
||||
|
||||
__all__ = ["artifacts", "assistants_compat", "mcp", "models", "recommended_questions", "skills", "suggestions", "threads", "thread_runs", "uploads"]
|
||||
__all__ = ["artifacts", "assistants_compat", "mcp", "models", "recommended_questions", "skills", "suggestions", "threads", "thread_runs", "uploads", "writing"]
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Writing rewrite API — SSE streaming endpoint.
|
||||
|
||||
POST /api/writing/rewrite
|
||||
Returns text/event-stream with frames:
|
||||
event: thinking data: {"chunk": "..."}
|
||||
event: text data: {"chunk": "..."}
|
||||
event: done data: {}
|
||||
event: error data: {"message": "..."}
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.gateway.deps import get_config
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.models import create_chat_model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/writing", tags=["writing"])
|
||||
|
||||
# ── Prompts ───────────────────────────────────────────────────────────────────
|
||||
|
||||
_SYSTEM_PROMPT = """你是一位专业的中文写作助手,擅长润色、扩写、缩写和调整文风。
|
||||
|
||||
规则:
|
||||
- 只输出改写后的正文,不加任何前缀、解释或括号说明。
|
||||
- 保持原文的语言(中文输入 → 中文输出,英文输入 → 英文输出)。
|
||||
- 改写结果应与原文长度相近,除非任务要求扩写或缩写。
|
||||
- 不要添加 markdown 标题或特殊格式,除非原文本身包含。
|
||||
"""
|
||||
|
||||
_ACTION_INSTRUCTIONS: dict[str, str] = {
|
||||
"polish": "请对以下文字进行润色,使其表达更流畅、自然,同时保留原意。",
|
||||
"expand": "请对以下文字进行扩写,补充细节和说明,使内容更丰富饱满。",
|
||||
"shorten": "请对以下文字进行缩写,保留核心意思,去除冗余表达,使其更简洁。",
|
||||
"tone": "请调整以下文字的语气风格。",
|
||||
}
|
||||
|
||||
_TONE_SUFFIX: dict[str, str] = {
|
||||
"professional": "目标风格:专业、正式。",
|
||||
"casual": "目标风格:轻松、口语化。",
|
||||
"readable": "目标风格:通俗易懂、易于阅读。",
|
||||
"academic": "目标风格:学术严谨、逻辑清晰。",
|
||||
"expressive": "目标风格:生动有文采、富有感染力。",
|
||||
}
|
||||
|
||||
|
||||
# ── Request model ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class SelectionInfo(BaseModel):
|
||||
from_: int = Field(..., alias="from", description="ProseMirror start offset")
|
||||
to: int = Field(..., description="ProseMirror end offset")
|
||||
text: str = Field(..., description="Selected plain text")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class SurroundingContext(BaseModel):
|
||||
before: str = Field(default="", description="Text before the selection")
|
||||
after: str = Field(default="", description="Text after the selection")
|
||||
|
||||
|
||||
class RewriteRequest(BaseModel):
|
||||
thread_id: str = Field(..., alias="threadId")
|
||||
selection: SelectionInfo
|
||||
action: str = Field(..., description="polish | expand | shorten | tone")
|
||||
tone: str | None = Field(default=None)
|
||||
model_name: str | None = Field(default=None, alias="modelName")
|
||||
surrounding_context: SurroundingContext = Field(default_factory=SurroundingContext, alias="surroundingContext")
|
||||
custom_instruction: str | None = Field(default=None, alias="customInstruction")
|
||||
follow_up_instruction: str | None = Field(default=None, alias="followUpInstruction")
|
||||
previous_result: str | None = Field(default=None, alias="previousResult")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _sse(event: str, data: dict) -> str:
|
||||
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||
|
||||
|
||||
def _extract_chunk_parts(chunk: object) -> tuple[str, str]:
|
||||
"""Return (thinking_delta, text_delta) from a LangChain streaming chunk."""
|
||||
thinking = ""
|
||||
text = ""
|
||||
|
||||
content = getattr(chunk, "content", "")
|
||||
if isinstance(content, str):
|
||||
text = content
|
||||
elif isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, str):
|
||||
text += block
|
||||
elif isinstance(block, dict):
|
||||
btype = block.get("type", "")
|
||||
if btype in ("thinking", "thinking_delta"):
|
||||
thinking += block.get("thinking", "")
|
||||
elif btype in ("text", "text_delta"):
|
||||
text += block.get("text", "")
|
||||
elif btype == "reasoning":
|
||||
thinking += block.get("content", "")
|
||||
|
||||
# DeepSeek / OpenAI o-series: reasoning in additional_kwargs
|
||||
if not thinking:
|
||||
kwargs = getattr(chunk, "additional_kwargs", {}) or {}
|
||||
r = kwargs.get("reasoning_content") or ""
|
||||
if isinstance(r, str):
|
||||
thinking = r
|
||||
|
||||
return thinking, text
|
||||
|
||||
|
||||
# ── Endpoint ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post(
|
||||
"/rewrite",
|
||||
summary="AI Text Rewrite (SSE stream)",
|
||||
response_class=StreamingResponse,
|
||||
)
|
||||
async def rewrite_selection(
|
||||
body: RewriteRequest,
|
||||
config: AppConfig = Depends(get_config),
|
||||
) -> StreamingResponse:
|
||||
action = body.action if body.action in _ACTION_INSTRUCTIONS else "polish"
|
||||
instruction = _ACTION_INSTRUCTIONS[action]
|
||||
if action == "tone" and body.tone and body.tone in _TONE_SUFFIX:
|
||||
instruction = f"{instruction}\n{_TONE_SUFFIX[body.tone]}"
|
||||
|
||||
context_parts: list[str] = []
|
||||
before = body.surrounding_context.before.strip()
|
||||
after = body.surrounding_context.after.strip()
|
||||
if before or after:
|
||||
context_parts.append("文章上下文(仅供参考,不要改写):")
|
||||
if before:
|
||||
context_parts.append(f"[前文] {before[-300:]}")
|
||||
if after:
|
||||
context_parts.append(f"[后文] {after[:300]}")
|
||||
context_parts.append("")
|
||||
|
||||
context_block = "\n".join(context_parts)
|
||||
base_instruction = instruction
|
||||
if body.custom_instruction:
|
||||
base_instruction = f"{instruction}\n用户额外要求:{body.custom_instruction}"
|
||||
user_message = f"{base_instruction}\n\n{context_block}需要改写的文字:\n{body.selection.text}"
|
||||
|
||||
if body.follow_up_instruction and body.previous_result:
|
||||
messages = [
|
||||
SystemMessage(content=_SYSTEM_PROMPT),
|
||||
HumanMessage(content=user_message),
|
||||
AIMessage(content=body.previous_result),
|
||||
HumanMessage(content=body.follow_up_instruction),
|
||||
]
|
||||
else:
|
||||
messages = [SystemMessage(content=_SYSTEM_PROMPT), HumanMessage(content=user_message)]
|
||||
|
||||
# Auto-enable thinking when the selected model supports it
|
||||
resolved_name = body.model_name or (config.models[0].name if config.models else None)
|
||||
model_cfg = config.get_model_config(resolved_name) if resolved_name else None
|
||||
thinking_enabled = bool(model_cfg and getattr(model_cfg, "supports_thinking", False))
|
||||
|
||||
async def generate():
|
||||
try:
|
||||
model = create_chat_model(name=body.model_name, thinking_enabled=thinking_enabled, app_config=config)
|
||||
async for chunk in model.astream(messages, config={"run_name": "writing_rewrite"}):
|
||||
thinking_delta, text_delta = _extract_chunk_parts(chunk)
|
||||
if thinking_delta:
|
||||
yield _sse("thinking", {"chunk": thinking_delta})
|
||||
if text_delta:
|
||||
yield _sse("text", {"chunk": text_delta})
|
||||
yield _sse("done", {})
|
||||
except Exception as e:
|
||||
logger.exception("writing/rewrite failed: thread=%s action=%s", body.thread_id, action)
|
||||
yield _sse("error", {"message": str(e)})
|
||||
|
||||
return StreamingResponse(
|
||||
generate(),
|
||||
media_type="text/event-stream",
|
||||
headers={"X-Accel-Buffering": "no", "Cache-Control": "no-cache"},
|
||||
)
|
||||
+42
@@ -24,6 +24,48 @@ Rules:
|
||||
- If the user is only asking a question (no edit needed), answer in text — skip the tool call.
|
||||
</canvas_behavior>
|
||||
|
||||
<clarification_system>
|
||||
Before creating a brand-new artifact or performing a major rewrite, verify you have enough
|
||||
information to do the job well. When key details are missing, call `ask_clarification` FIRST
|
||||
— do NOT start generating the artifact and ask questions mid-way.
|
||||
|
||||
**MUST clarify when (no existing artifact or major rewrite requested):**
|
||||
1. OUTPUT TYPE IS VAGUE
|
||||
- "帮我做个主页" / "make a homepage" → ask: tech stack (plain HTML / React / Vue?),
|
||||
design style, which sections to include
|
||||
- "写个报告" / "write a report" → ask: topic, target audience, expected length,
|
||||
required sections
|
||||
- "写个脚本" / "write a script" → ask: programming language, what it should do
|
||||
2. REQUIRED CONTENT IS MISSING
|
||||
- User wants a personal profile page but hasn't provided their name / bio / links
|
||||
- User wants a data table but hasn't provided the data or schema
|
||||
3. AMBIGUOUS APPROACH WITH SIGNIFICANTLY DIFFERENT OUTCOMES
|
||||
- "Design a chat system" → ask: client–server or peer-to-peer, persistence needed?
|
||||
- "Write an intro" → ask: formal essay or casual blog post?
|
||||
|
||||
**DO NOT clarify for:**
|
||||
- Simple edits to an existing artifact: "make it bold", "fix the typo", "shorten paragraph 2"
|
||||
- Formatting / style tweaks: "add comments", "use camelCase", "format the code"
|
||||
- Quick actions: expand / shorten / formalize / casualize / add_comments / fix_bugs / optimize_code
|
||||
- Requests where intent is clear from context (e.g., user pastes content and says "translate this")
|
||||
- Follow-up turns where enough context already exists in the conversation
|
||||
|
||||
**Format for clarification (use options when practical):**
|
||||
ask_clarification(
|
||||
question="你希望这个主页使用什么技术栈?",
|
||||
clarification_type="missing_info",
|
||||
options=[
|
||||
{{"id": "html", "label": "纯 HTML / CSS / JS", "description": "无框架,最简单"}},
|
||||
{{"id": "react", "label": "React", "description": "组件化,适合复杂交互"}},
|
||||
{{"id": "vue", "label": "Vue", "description": "渐进式框架,上手容易"}},
|
||||
],
|
||||
allow_custom=True,
|
||||
)
|
||||
|
||||
CRITICAL: clarification ALWAYS comes before generating the artifact. Never produce a partial
|
||||
artifact and then ask for clarification in the same response.
|
||||
</clarification_system>
|
||||
|
||||
<artifact_context>
|
||||
The current artifact state is injected into your context. When editing, build on the existing content exactly.
|
||||
When creating from scratch, produce a complete, high-quality artifact in one shot.
|
||||
|
||||
Reference in new issue
Block a user