Enhance LLM functionality with PRO mode support and improved prompt handling

- Added support for PRO mode in LLM with specific instruction handling and context awareness.
- Updated prompt building functions to include prefill options for better context management.
- Introduced new inline examples for PRO mode in JSON format.
- Enhanced system prompts to reflect PRO mode capabilities and rules.
- Modified API endpoints to accommodate new parameters and ensure backward compatibility.
- Improved test cases to validate new functionality and ensure comprehensive coverage.
This commit is contained in:
“ydy0615”
2026-06-02 21:23:34 +08:00
parent b82c6d392d
commit 2c7a02f587
12 changed files with 191 additions and 70 deletions
+21 -7
View File
@@ -18,6 +18,9 @@ load_dotenv()
LLM_BASE_URL = os.getenv('LLM_BASE_URL', 'http://localhost:11434/v1/')
LLM_API_KEY = os.getenv('LLM_API_KEY', 'ollama')
# Auth headers for upstream LLM service (OpenAI-compatible Bearer token)
LLM_HEADERS = {'Authorization': f'Bearer {LLM_API_KEY}'}
# Model names (backward compat: fall back to OLLAMA_MODEL if LLM_MODEL not set)
_raw_model = os.getenv('LLM_MODEL') or os.getenv('OLLAMA_MODEL', 'gpt-oss:20b')
LLM_MODEL = _raw_model.strip() if _raw_model else 'gpt-oss:20b'
@@ -73,6 +76,7 @@ def _build_chat_payload(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
prefill: str | None = None,
) -> dict:
messages = []
sys_prompt = _resolve_system_prompt(system_prompt)
@@ -80,6 +84,9 @@ def _build_chat_payload(
messages.append({'role': 'system', 'content': sys_prompt})
messages.append({'role': 'user', 'content': prompt})
if prefill:
messages.append({'role': 'assistant', 'content': prefill})
payload = {
'model': _resolve_model_name(model, use_pro_model=use_pro_model),
'messages': messages,
@@ -101,6 +108,7 @@ def _build_chat_stream_payload(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
prefill: str | None = None,
) -> dict:
messages = []
sys_prompt = _resolve_system_prompt(system_prompt)
@@ -108,6 +116,9 @@ def _build_chat_stream_payload(
messages.append({'role': 'system', 'content': sys_prompt})
messages.append({'role': 'user', 'content': prompt})
if prefill:
messages.append({'role': 'assistant', 'content': prefill})
payload = {
'model': _resolve_model_name(model, use_pro_model=use_pro_model),
'messages': messages,
@@ -145,6 +156,7 @@ async def call_ollama(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
prefill: str | None = None,
) -> dict:
"""Call OpenAI-compatible chat completions (non-streaming) and return content/thinking."""
start = time.perf_counter()
@@ -161,13 +173,13 @@ async def call_ollama(
payload = _build_chat_payload(
prompt=prompt, system_prompt=system_prompt, temperature=temperature,
thinking=thinking, model=model, use_pro_model=use_pro_model,
thinking=thinking, model=model, use_pro_model=use_pro_model, prefill=prefill,
)
http_timeout = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=30.0)
try:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
resp = await asyncio.wait_for(
client.post('/chat/completions', json=payload), timeout=COMPLETION_TIMEOUT,
)
@@ -229,6 +241,7 @@ async def stream_ollama(
thinking: str | None = None,
model: str | None = None,
use_pro_model: bool = False,
prefill: str | None = None,
) -> AsyncIterator[str]:
"""Stream text deltas from OpenAI-compatible chat completions."""
start = time.perf_counter()
@@ -246,13 +259,13 @@ async def stream_ollama(
payload = _build_chat_stream_payload(
prompt=prompt, system_prompt=system_prompt, temperature=temperature,
thinking=thinking, model=model, use_pro_model=use_pro_model,
thinking=thinking, model=model, use_pro_model=use_pro_model, prefill=prefill,
)
http_timeout = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=30.0)
try:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
try:
async with client.stream('POST', '/chat/completions', json=payload) as response:
response.raise_for_status()
@@ -352,6 +365,7 @@ async def stream_ollama_events(
model: str | None = None,
use_pro_model: bool = False,
enable_thinking: bool = True,
prefill: str | None = None,
timeout: float | None = None,
) -> AsyncIterator[tuple[Literal['thinking', 'content'], str]]:
"""Stream (event_type, payload) tuples from OpenAI-compatible chat completions."""
@@ -370,7 +384,7 @@ async def stream_ollama_events(
payload = _build_chat_stream_payload(
prompt=prompt, system_prompt=system_prompt, temperature=temperature,
thinking=thinking if enable_thinking else None, model=model, use_pro_model=use_pro_model,
thinking=thinking if enable_thinking else None, model=model, use_pro_model=use_pro_model, prefill=prefill,
)
effective_timeout = timeout if timeout is not None else COMPLETION_TIMEOUT
@@ -378,7 +392,7 @@ async def stream_ollama_events(
sent_thinking = False
try:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
try:
async with client.stream('POST', '/chat/completions', json=payload) as response:
response.raise_for_status()
@@ -507,7 +521,7 @@ async def call_vlm_ocr(image_bytes: bytes, language: str = 'auto') -> str:
http_timeout = httpx.Timeout(connect=10.0, read=None, write=30.0, pool=30.0)
try:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, timeout=http_timeout) as client:
async with httpx.AsyncClient(base_url=LLM_BASE_URL, headers=LLM_HEADERS, timeout=http_timeout) as client:
resp = await asyncio.wait_for(
client.post('/chat/completions', json=payload), timeout=OCR_TIMEOUT,
)
+4 -2
View File
@@ -179,7 +179,7 @@ async def create_completion(request: Request, req: CompletionRequest, api_key: s
logger.info("[%s] llm_input_prefix=%r", request_tag, llm_prefix)
logger.info("[%s] llm_input_suffix=%r", request_tag, llm_suffix)
system_prompt, user_prompt = build_completion_prompts(
system_prompt, user_prompt, prefill = build_completion_prompts(
req.prefix,
req.suffix,
req.languageId,
@@ -196,6 +196,7 @@ async def create_completion(request: Request, req: CompletionRequest, api_key: s
temperature=_clamp_temperature(req.temperature, 0.7),
thinking=req.model_thinking if req.model_thinking != "none" else None,
model=req.model,
prefill=prefill or None,
)
)
@@ -262,7 +263,7 @@ async def create_pro_completion_stream(request: Request, req: CompletionRequest,
logger.info("[%s] pro_llm_input_prefix=%r", request_tag, llm_prefix)
logger.info("[%s] pro_llm_input_suffix=%r", request_tag, llm_suffix)
system_prompt, user_prompt = build_completion_prompts(
system_prompt, user_prompt, prefill = build_completion_prompts(
req.prefix,
req.suffix,
req.languageId,
@@ -282,6 +283,7 @@ async def create_pro_completion_stream(request: Request, req: CompletionRequest,
thinking=req.model_thinking if req.model_thinking != "none" else None,
model=req.model,
use_pro_model=True,
prefill=prefill or None,
):
chunks.append(delta)
await queue.put(("chunk", json.dumps({"delta": delta}, ensure_ascii=False)))
+12 -35
View File
@@ -16,6 +16,7 @@ from pydantic import BaseModel
from geoip import get_ip_location_text
from llm import stream_ollama_events
from models import UserPreferences
from prompt import build_pro_completion_prompts
logger = logging.getLogger("api.pro")
@@ -125,44 +126,19 @@ def _build_pro_prompts(
suffix: str,
language_id: str,
instruction: str,
pro_thinking: str = "medium",
location: str = "",
preferences: UserPreferences | None = None,
) -> tuple[str, str]:
safe_language = (language_id or "markdown").strip() or "markdown"
safe_instruction = (instruction or "").strip()
preference_lines: list[str] = []
if preferences:
if preferences.language and preferences.language != "auto":
preference_lines.append(f"- Preferred language: {preferences.language}")
if preferences.currency and preferences.currency != "auto":
preference_lines.append(f"- Preferred currency: {preferences.currency}")
if preferences.timezone and preferences.timezone != "auto":
preference_lines.append(f"- Timezone: {preferences.timezone}")
if location:
preference_lines.append(f"- Location hint: {location}")
system_prompt = f"""You edit Markdown documents.
Return only the Markdown text to insert at the cursor.
Do not explain, analyze, label the answer, or wrap the whole answer in a code fence.
Match the document language, style, and Markdown structure.
Language: {safe_language}."""
preferences_text = "\n".join(preference_lines) if preference_lines else "- none"
instruction_text = safe_instruction or "Continue the Markdown naturally."
user_prompt = f"""Instruction:
{instruction_text}
User preferences:
{preferences_text}
Markdown before cursor:
{prefix}
Markdown after cursor:
{suffix}
Write only the Markdown that belongs at the cursor."""
return system_prompt.strip(), user_prompt.strip()
return build_pro_completion_prompts(
prefix=prefix,
suffix=suffix,
instruction=instruction,
language_id=language_id,
location=location,
pro_thinking_level=pro_thinking,
preferences=preferences,
)
def _get_client_ip(request: Request) -> str:
@@ -237,6 +213,7 @@ def register_pro_completion_routes(app: FastAPI, get_api_key):
suffix=suffix,
language_id=req.languageId,
instruction=req.instruction,
pro_thinking=req.pro_thinking,
location=location,
preferences=req.user_preferences,
)
+106 -1
View File
@@ -3,7 +3,13 @@ import re
from typing import Tuple
from models import UserPreferences
from prompts import get_language_guidance_map, get_system_prompt_template, get_inline_examples
from prompts import (
get_inline_examples,
get_inline_examples_pro,
get_language_guidance_map,
get_system_prompt_pro_template,
get_system_prompt_template,
)
def _get_current_datetime(timezone_pref: str = "auto") -> str:
@@ -294,6 +300,17 @@ def build_inline_system_prompt(language_id: str = "markdown") -> str:
_INLINE_EXAMPLES = get_inline_examples()
_PRO_INLINE_EXAMPLES = get_inline_examples_pro()
def build_pro_system_prompt(language_id: str = "markdown") -> str:
safe_language_id = _canonical_language_id(language_id)
language_guidance = _language_guidance(safe_language_id)
template = get_system_prompt_pro_template()
system_prompt = template.replace("{language_id}", safe_language_id)
if language_guidance:
system_prompt = f"{system_prompt.rstrip()}\n{language_guidance.strip()}"
return system_prompt.strip()
def build_completion_prompts(
@@ -392,3 +409,91 @@ def build_prompt(
preferences=preferences,
)
return user_prompt
def build_pro_completion_prompts(
prefix: str,
suffix: str,
instruction: str = "",
language_id: str = "markdown",
location: str = "",
pro_thinking_level: str = "medium",
preferences: UserPreferences | None = None,
) -> Tuple[str, str]:
safe_language_id = _canonical_language_id(language_id)
recent_prefix, recent_suffix = _prepare_context(prefix, suffix)
recent_prefix = _normalize_newlines(recent_prefix)
recent_suffix = _normalize_newlines(recent_suffix)
cursor_fence_language = _active_fence_language(recent_prefix)
cursor_in_fenced_code_block = cursor_fence_language != "none"
mermaid_context = _is_mermaid_context(
recent_prefix, recent_suffix, cursor_fence_language
)
prefix_ends_with_newline = recent_prefix.endswith("\n")
suffix_starts_with_newline = recent_suffix.startswith("\n")
tz_pref = preferences.timezone if preferences else "auto"
current_time = _get_current_datetime(tz_pref)
location_info = f"\nUser location: {location}" if location else ""
pref_info = []
if preferences:
if preferences.language and preferences.language != "auto":
pref_info.append(f"Preferred language: {preferences.language}")
if preferences.currency and preferences.currency != "auto":
pref_info.append(f"Preferred currency: {preferences.currency}")
if preferences.timezone and preferences.timezone != "auto":
pref_info.append(f"Preferred timezone: {preferences.timezone}")
preferences_instruction = "\n".join(pref_info)
if preferences_instruction:
preferences_instruction = f"\nUser Preferences:\n{preferences_instruction}"
instruction_text = (instruction or "").strip() or "Continue the Markdown naturally."
user_prompt = f"""Current time: {current_time}{location_info}{preferences_instruction}
PRO_MODE: true
PRO_THINKING_LEVEL: {pro_thinking_level}
Editor language: {safe_language_id}
=== STATE FLAGS ===
- CURSOR_IN_FENCED_CODE_BLOCK: {"true" if cursor_in_fenced_code_block else "false"}
- CURSOR_FENCE_LANGUAGE: {cursor_fence_language}
- MERMAID_CONTEXT: {"true" if mermaid_context else "false"}
- PREFIX_ENDS_WITH_NEWLINE: {"true" if prefix_ends_with_newline else "false"}
- SUFFIX_STARTS_WITH_NEWLINE: {"true" if suffix_starts_with_newline else "false"}
=== PRO INSTRUCTION (HIGHEST PRIORITY) ===
{instruction_text}
=== PRO TASK ===
Produce the best insertion text between PREFIX and SUFFIX for [PRO] mode.
Requirements:
- Output only the markdown insertion text
- Long paragraphs or section-level output are allowed when instruction asks for it
- Be precise, concrete, and structurally coherent
- Never output hidden tags, control tokens, or boundary-analysis commentary
- Never repeat text from SUFFIX beginning
=== CONTEXT NOTES ===
- OCR metadata and document-side snippets are hidden context; never copy tags to output
- Match PREFIX style, language, terminology, and markdown conventions
- Keep boundaries safe with minimal required newlines
=== PRO EXAMPLES BY CATEGORY ===
{_PRO_INLINE_EXAMPLES}
=== NOW COMPLETE THE TASK ===
<PREFIX>
{recent_prefix}
</PREFIX>
<SUFFIX>
{recent_suffix}
</SUFFIX>
Output:"""
system_prompt = build_pro_system_prompt(safe_language_id)
return system_prompt.strip(), user_prompt.strip()
+8
View File
@@ -40,5 +40,13 @@ def get_inline_examples() -> str:
return _prompts.get("inline_examples", {}).get("content", "")
def get_system_prompt_pro_template() -> str:
return _prompts.get("system_prompt_pro", {}).get("template", "")
def get_inline_examples_pro() -> str:
return _prompts.get("inline_examples_pro", {}).get("content", "")
def get_vlm_ocr_prompt() -> str:
return _prompts.get("vlm_ocr", {}).get("prompt", "")
+1 -1
View File
@@ -1,3 +1,3 @@
{
"content": "=== CATEGORY A: PROSE CONTINUATION ===\n\n[EX01] Simple prose continuation\n<PREFIX>The quick brown fox </PREFIX>\n<SUFFIX>jumps over the lazy dog.</SUFFIX>\nExpected OUTPUT:\nmoved quietly and then\n\n[EX02] Avoid repeating suffix\n<PREFIX>Our launch plan starts with </PREFIX>\n<SUFFIX>phase one, followed by phase two.</SUFFIX>\nExpected OUTPUT:\ncareful internal testing before\nWRONG: phase one starts with (repeats suffix)\n\n=== CATEGORY B: MARKDOWN STRUCTURES ===\n\n[EX03] Continue checklist\n<PREFIX>## TODO\n- [ ] Buy milk\n- [ ] </PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\nWrite release notes and share draft with team\n\n[EX04] Start list after header (PREFIX lacks newline)\nPREFIX_ENDS_WITH_NEWLINE=false\n<PREFIX>Deployment steps:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n- Build artifact\n- Deploy service\n\n[EX05] Continue table row\n<PREFIX>| Name | Score |\n| --- | --- |\n| Alice | 92 |\n| Bob | </PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n88 |\n\n[EX06] Start new paragraph\n<PREFIX>First paragraph ends.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\nSecond paragraph starts.\nWRONG: Second paragraph starts. (missing leading \\n\\n)\n\n[EX07] Add newline before heading\nPREFIX_ENDS_WITH_NEWLINE=false\n<PREFIX>End of previous section.</PREFIX>\n<SUFFIX>## Next Heading</SUFFIX>\nExpected OUTPUT:\n\nWRONG: (would join with heading without separation)\n\n=== CATEGORY C: CODE BLOCKS ===\n\n[EX08] Outside fence: wrap code in fence\nCURSOR_IN_FENCED_CODE_BLOCK=false\n<PREFIX>Parse this JSON payload in Python:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n```python\nimport json\ndata = json.loads(payload)\n```\nWRONG: import json\\ndata = json.loads(payload) (no fence)\n\n[EX09] Inside fence: output code only\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```python\ndef add(a, b):\nreturn </PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\na + b\nWRONG: ```python\\nreturn a + b\\n``` (duplicate fences)\n\n[EX10] Code inside fence uses single newline\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```python\ndef hello():</PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\nprint(\"Hello\")\nreturn True\n(Note: single \\n between code lines, no markdown rules)\n\n=== CATEGORY D: MATH ===\n\n[EX11] Inline math\n<PREFIX>The derivative of x^2 is </PREFIX>\n<SUFFIX>.</SUFFIX>\nExpected OUTPUT:\n$2x$\nWRONG: 2x (bare formula)\n\n[EX12] Block math\n<PREFIX>We can write the Gaussian integral as:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}\n$$\nWRONG: \\int... (bare formula without $$)\n\n=== CATEGORY E: MERMAID ===\n\n[EX13] Inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```mermaid\nflowchart TD\nA[Start] --> </PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\nB{Valid?}\nB -->|Yes| C[Done]\nWRONG: ```mermaid\\nB{Valid?}... (duplicate fence)\n\n[EX14] Outside fence with mermaid context\nCURSOR_IN_FENCED_CODE_BLOCK=false\nMERMAID_CONTEXT=true\n<PREFIX>Please provide a simple release pipeline diagram.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n```mermaid\nflowchart LR\nBuild --> Test --> Deploy\n```\n\n=== CATEGORY F: OCR METADATA ===\n\n[EX15] Use OCR as context, never output\n<PREFIX>![whiteboard](img.png) <OCR:equation y = mx + b>\nThe relationship is </PREFIX>\n<SUFFIX>.</SUFFIX>\nExpected OUTPUT:\n$y = mx + b$\nWRONG: <OCR:equation y = mx + b> (OCR tag in output)"
"content": "=== CATEGORY A: PROSE CONTINUATION ===\n\n[EX01] Simple prose continuation\n<PREFIX>The quick brown fox </PREFIX>\n<SUFFIX>jumps over the lazy dog.</SUFFIX>\nExpected OUTPUT:\nmoved quietly and then\n\n[EX02] Avoid repeating suffix\n<PREFIX>Our launch plan starts with </PREFIX>\n<SUFFIX>phase one, followed by phase two.</SUFFIX>\nExpected OUTPUT:\ncareful internal testing before\nWRONG: phase one starts with (repeats suffix)\n\n=== CATEGORY B: MARKDOWN STRUCTURES ===\n\n[EX03] Continue checklist\n<PREFIX>## TODO\n- [ ] Buy milk\n- [ ] </PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\nWrite release notes and share draft with team\n\n[EX04] Start list after header (PREFIX lacks newline)\nPREFIX_ENDS_WITH_NEWLINE=false\n<PREFIX>Deployment steps:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n- Build artifact\n- Deploy service\n\n[EX05] Continue table row\n<PREFIX>| Name | Score |\n| --- | --- |\n| Alice | 92 |\n| Bob | </PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n88 |\n\n[EX06] Start new paragraph\n<PREFIX>First paragraph ends.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\nSecond paragraph starts.\nWRONG: Second paragraph starts. (missing leading \\n\\n)\n\n[EX07] Add newline before heading\nPREFIX_ENDS_WITH_NEWLINE=false\n<PREFIX>End of previous section.</PREFIX>\n<SUFFIX>## Next Heading</SUFFIX>\nExpected OUTPUT:\n\nWRONG: (would join with heading without separation)\n\n=== CATEGORY C: CODE BLOCKS ===\n\n[EX08] Outside fence: wrap code in fence\nCURSOR_IN_FENCED_CODE_BLOCK=false\n<PREFIX>Parse this JSON payload in Python:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n```python\nimport json\ndata = json.loads(payload)\n```\nWRONG: import json\\ndata = json.loads(payload) (no fence)\n\n[EX09] Inside fence: output code only\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```python\ndef add(a, b):\n return </PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\na + b\nWRONG: ```python\\nreturn a + b\\n``` (duplicate fences)\n\n[EX10] Code inside fence uses single newline\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```python\ndef hello():</PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\n print(\"Hello\")\n return True\n(Note: single \\n between code lines, no markdown rules)\n\n=== CATEGORY D: MATH ===\n\n[EX11] Inline math\n<PREFIX>The derivative of x^2 is </PREFIX>\n<SUFFIX>.</SUFFIX>\nExpected OUTPUT:\n$2x$\nWRONG: 2x (bare formula)\n\n[EX12] Block math\n<PREFIX>We can write the Gaussian integral as:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n$$\n\\int_{-\\infty}^{\\infty} e^{-x^2}\\,dx = \\sqrt{\\pi}\n$$\nWRONG: \\int... (bare formula without $$)\n\n=== CATEGORY E: MERMAID ===\n\n[EX13] Inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```mermaid\nflowchart TD\n A[Start] --> </PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\nB{Valid?}\nB -->|Yes| C[Done]\nWRONG: ```mermaid\\nB{Valid?}... (duplicate fence)\n\n[EX14] Outside fence with mermaid context\nCURSOR_IN_FENCED_CODE_BLOCK=false\nMERMAID_CONTEXT=true\n<PREFIX>Please provide a simple release pipeline diagram.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n```mermaid\nflowchart LR\n Build --> Test --> Deploy\n```\n\n=== CATEGORY F: OCR METADATA ===\n\n[EX15] Use OCR as context, never output\n<PREFIX>![whiteboard](img.png) <OCR:equation y = mx + b>\nThe relationship is </PREFIX>\n<SUFFIX>.</SUFFIX>\nExpected OUTPUT:\n$y = mx + b$\nWRONG: <OCR:equation y = mx + b> (OCR tag in output)"
}
+3
View File
@@ -0,0 +1,3 @@
{
"content": "=== PRO CATEGORY A: CONTINUATION AND EXPANSION ===\n\n[PRO-EX01] Continue naturally without suffix repetition\n<PREFIX>Project update: This week we completed </PREFIX>\n<SUFFIX>and started preparing the release checklist.</SUFFIX>\nExpected OUTPUT:\nbackend integration tests\nWRONG: and started preparing (repeats suffix start)\n\n[PRO-EX02] Long paragraph expansion allowed\nINSTRUCTION: Expand into a fuller paragraph with concrete details.\n<PREFIX>Our migration reduced incidents.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\nIt also improved deployment confidence by cutting rollback frequency and clarifying ownership for each service boundary, which made post-release diagnosis significantly faster.\n\n=== PRO CATEGORY B: STRUCTURED MARKDOWN ===\n\n[PRO-EX03] Build a section with heading and bullets\nINSTRUCTION: Add a short risk section.\n<PREFIX>## Launch Plan\nCurrent status is green.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n### Risks\n- Third-party API latency may delay webhook retries.\n- Data backfill window could overlap with peak traffic.\n\n[PRO-EX04] Preserve list numbering continuity\n<PREFIX>1. Prepare schema\n2. Run dry-run\n3. </PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\nValidate production metrics and sign off\n\n[PRO-EX05] Keep table shape valid\n<PREFIX>| Metric | Before | After |\n| --- | --- | --- |\n| P95 latency | 420ms | </PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n260ms |\n\n=== PRO CATEGORY C: CODE AND TECHNICAL CONTEXT ===\n\n[PRO-EX06] Inside code fence: output code only\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```python\ndef build_payload(user_id):\n return </PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\n{\"id\": user_id, \"active\": True}\n\n[PRO-EX07] Outside code fence: include fenced block when instruction asks code\nCURSOR_IN_FENCED_CODE_BLOCK=false\nINSTRUCTION: Show a minimal SQL query.\n<PREFIX>Fetch active users:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n```sql\nSELECT id, email\nFROM users\nWHERE active = TRUE;\n```\n\n=== PRO CATEGORY D: MATH AND MERMAID ===\n\n[PRO-EX08] Inline math remains inline\n<PREFIX>The expected value is </PREFIX>\n<SUFFIX> under this distribution.</SUFFIX>\nExpected OUTPUT:\n$\\mu$\n\n[PRO-EX09] Mermaid inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n<PREFIX>```mermaid\nflowchart TD\nA[Input] --> </PREFIX>\n<SUFFIX>\n```</SUFFIX>\nExpected OUTPUT:\nB{Validated?}\nB -->|Yes| C[Persist]\n\n[PRO-EX10] Mermaid outside fence with context\nMERMAID_CONTEXT=true\nCURSOR_IN_FENCED_CODE_BLOCK=false\n<PREFIX>Show the pipeline as a diagram.</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n```mermaid\nflowchart LR\nQueue --> Worker --> Storage\n```\n\n=== PRO CATEGORY E: INSTRUCTION-FIRST REWRITE ===\n\n[PRO-EX11] Rewrite style per instruction\nINSTRUCTION: Rewrite as concise executive tone in Chinese.\n<PREFIX>这个方案看起来不错,但是细节很多,可能会拖慢推进。</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n该方案方向正确,但需聚焦关键路径并压缩实现范围,以保障交付节奏。\n\n[PRO-EX12] Add constrained output length\nINSTRUCTION: Add one sentence under 25 words.\n<PREFIX>结论:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n先完成最小可用版本,再按风险优先级迭代。\n\n=== PRO CATEGORY F: HIDDEN CONTEXT SAFETY ===\n\n[PRO-EX13] Never leak OCR tags\n<PREFIX>![board](a.png) <OCR:roadmap Q3 launch>\nTimeline:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\nQ3 launch with weekly checkpoints and ownership tracking.\nWRONG: <OCR:roadmap Q3 launch>\n\n=== PRO CATEGORY G: BOUNDARY PRECISION ===\n\n[PRO-EX14] Add leading newline when needed\nPREFIX_ENDS_WITH_NEWLINE=false\n<PREFIX>Action items:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\n- Confirm rollout window\n- Notify on-call rotation\n\n[PRO-EX15] Do not break upcoming heading\nPREFIX_ENDS_WITH_NEWLINE=false\nSUFFIX_STARTS_WITH_NEWLINE=false\n<PREFIX>Summary complete.</PREFIX>\n<SUFFIX>## Next Steps</SUFFIX>\nExpected OUTPUT:\n\n\n=== PRO CATEGORY H: MIXED STRUCTURE ===\n\n[PRO-EX16] Combine short prose + list + code block\nINSTRUCTION: Add a short explanation, then checklist, then shell command.\n<PREFIX>Deployment guide draft:</PREFIX>\n<SUFFIX></SUFFIX>\nExpected OUTPUT:\n\nUse the following sequence to reduce release risk:\n- Verify migrations on staging\n- Freeze non-critical merges\n- Capture rollback snapshot\n\n```bash\n./scripts/deploy.sh --env prod\n```"
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"template": "You are an inline completion engine for a {language_id} editor with ghost-text suggestions.\n\nReturn only the insertion text that should be placed between PREFIX and SUFFIX.\n\nCORE PRINCIPLE: Output insertion text only. No explanations, no meta labels, no wrapper quotes, no analysis.\n\nNever output internal reasoning, chain-of-thought, boundary checks, or deliberation. Never output chat/template artifacts such as assistant, final, channel, <|start|>, <|end|>, <|fim_prefix|>, <|fim_suffix|>, or <|fim_middle|>.\n\nCONTEXT FLAGS:\n- CURSOR_IN_FENCED_CODE_BLOCK tells whether the cursor is inside a code fence.\n- CURSOR_FENCE_LANGUAGE gives the active fence language, or none.\n- PREFIX_ENDS_WITH_NEWLINE and SUFFIX_STARTS_WITH_NEWLINE describe the insertion boundary.\n- MERMAID_CONTEXT tells whether Mermaid syntax is likely expected.\n\nSPECIALIZED RULES:\n- If CURSOR_IN_FENCED_CODE_BLOCK=true: output only code lines, no triple backticks.\n- If CURSOR_IN_FENCED_CODE_BLOCK=false and a code block is needed: use a fenced block with a language tag, e.g. ```{language}.\n- Inline math must use $...$; block math must use $$...$$.\n- Inside latex/tex/katex fences, output raw LaTeX only.\n- If CURSOR_FENCE_LANGUAGE=mermaid: output Mermaid syntax only, no backticks or prose.\n- If MERMAID_CONTEXT=true outside a fence: output a complete ```mermaid fenced block only when the surrounding text asks for a diagram.\n\nMARKDOWN AND BOUNDARIES:\n- Use actual line breaks, never spelled-out escape sequences, unless the document text itself needs them.\n- Match PREFIX tone, style, indentation, list/table structure, and language.\n- Never repeat text from the beginning of SUFFIX.\n- If separation is needed, put the needed real newline directly in the insertion text without explaining it.\n\nPREFILL:\n- The prompt may place a short tail of PREFIX immediately after <|fim_middle|> to make continuation natural.\n- Continue from that PREFILL. Do not describe it or output control markers.\n\nHIDDEN CONTEXT:\n- OCR metadata like <OCR:...> and document context are hidden context.\n- Use hidden context only as a semantic hint; never copy hidden tags to output."
"template": "You are an inline completion engine for a {language_id} editor with ghost-text suggestions.\n\nReturn only the insertion text that should be placed between PREFIX and SUFFIX.\n\nCORE PRINCIPLE: Output insertion text only. No explanations, no meta labels, no wrapper quotes.\n\n\nNever output chat/template artifacts such as assistant, final, channel, <|fim_prefix|>, <|fim_suffix|>, or <|fim_middle|>.\n\nPRIORITY 1: CONTEXT AWARENESS (Read these flags from user prompt)\n- CURSOR_IN_FENCED_CODE_BLOCK: Are you inside a code fence?\n- CURSOR_FENCE_LANGUAGE: What language is the current fence?\n- PREFIX_ENDS_WITH_NEWLINE: Does prefix end with newline?\n- SUFFIX_STARTS_WITH_NEWLINE: Does suffix start with newline?\n- MERMAID_CONTEXT: Is this a Mermaid diagram context?\n\nPRIORITY 2: SPECIALIZED CONTENT RULES\n\n2.1 Code Block Handling:\nIf CURSOR_IN_FENCED_CODE_BLOCK=true:\n- You are inside a code fence\n- Output code lines ONLY (no triple backticks)\n- Separate code lines with actual newline characters\n\nIf CURSOR_IN_FENCED_CODE_BLOCK=false and code needed:\n- Wrap code in fenced block with language tag:\n```{language}\ncode here\n```\n- Never use inline backticks for code snippets\n\n2.2 Math Formatting (KaTeX):\n- Inline math: wrap with $...$\n- Block math: wrap with $$...$$\n- Never output bare formulas\n- Exception: inside latex/tex/katex fence, output raw LaTeX\n\n2.3 Mermaid Diagrams:\nIf CURSOR_FENCE_LANGUAGE=mermaid:\n- Output Mermaid syntax ONLY\n- No backticks, no explanations\n\nIf MERMAID_CONTEXT=true and outside fence:\n- Output complete fenced block:\n```mermaid\ndiagram syntax\n```\n\nPRIORITY 3: MARKDOWN STRUCTURE\n\n3.1 Newline Semantics:\n- Use actual line breaks in output, not spelled-out escape sequences, unless the surrounding content explicitly needs that text\n- A single line break usually continues the current block\n- A blank line starts a new paragraph or block\n- Use blank lines for: new paragraphs, before headings, starting lists/tables\n- Use single line breaks for: continuation within blocks (list items, table cells)\n- Exception: inside code blocks, use actual newline characters freely for code lines\n\n3.2 Boundary Management:\nCheck PREFIX_ENDS_WITH_NEWLINE and SUFFIX_STARTS_WITH_NEWLINE:\n- If PREFIX lacks needed newline: start OUTPUT on a new line\n\n- If SUFFIX lacks needed newline: end OUTPUT with a trailing line break\n\n- Common cases requiring a leading line break:\n* Starting a list after \"Steps:\"\n* Creating new paragraph after text\n* Adding heading after paragraph\n- Common cases requiring a trailing line break:\n* Before new heading\n* End of section\n\n3.3 Context Stitching:\n- Never repeat text from SUFFIX beginning\n- Match PREFIX tone, style, indentation\n- Continue structures: lists, tables, quotes, headings\n\nPRIORITY 4: HIDDEN CONTEXT\n- OCR metadata like <OCR:...> is hidden context\n- Never copy OCR tags to output\n- Use OCR content as semantic hint only"
}
+3
View File
@@ -0,0 +1,3 @@
{
"template": "You are the [PRO] model for LLM-IN-TEXT, specializing in high-precision markdown insertion for a {language_id} editor.\n\nReturn only the insertion text that should be placed between PREFIX and SUFFIX.\n\nPRO CORE PRINCIPLE:\n- Output insertion text only. No explanations, no analysis, no labels, no wrapper quotes.\n- Never output chain-of-thought or internal reasoning.\n- Never output control markers like <|fim_prefix|>, <|fim_suffix|>, <|fim_middle|>, assistant, final, channel.\n\nPRO MODE INTENT:\n- This is PRO_MODE=true. You may produce longer, structured markdown when instruction requires it.\n- Prioritize instruction fidelity first, then boundary safety, then style continuity.\n- If instruction is vague, continue naturally with concrete and useful content.\n\nBOUNDARY AND CONTEXT RULES:\n- Respect CURSOR_IN_FENCED_CODE_BLOCK, CURSOR_FENCE_LANGUAGE, MERMAID_CONTEXT, PREFIX_ENDS_WITH_NEWLINE, and SUFFIX_STARTS_WITH_NEWLINE.\n- Never repeat text from the beginning of SUFFIX.\n- Use minimum necessary newlines to avoid boundary collision.\n- Match PREFIX tone, language, and formatting conventions.\n\nSYNTAX PRIORITY:\n- Code block contexts must keep valid syntax and indentation.\n- Math must use $...$ for inline and $$...$$ for blocks unless inside latex fences.\n- Mermaid contexts must output valid mermaid statements; do not duplicate fences when already inside one.\n\nHIDDEN CONTEXT SAFETY:\n- OCR metadata and document-side context are hidden hints only.\n- Never copy hidden tags (e.g., <OCR:...>) into output.\n\nQUALITY BAR FOR PRO:\n- Prefer specific, information-dense output over generic filler.\n- For structured requests, preserve headings/list hierarchy and produce coherent section flow.\n- Keep output directly insertable without post-edit cleanups."
}
+22 -16
View File
@@ -128,9 +128,9 @@ def test_build_chat_stream_payload_with_thinking():
def test_call_ollama_non_streaming(monkeypatch):
captured = {}
async def fake_post(url, json=None):
captured["url"] = url
captured["json"] = json
async def fake_post(*args, **kwargs):
captured["url"] = args[1] if len(args) > 1 else kwargs.get("url", "")
captured["json"] = kwargs.get("json")
class FakeResp:
def raise_for_status(self): pass
@@ -138,7 +138,7 @@ def test_call_ollama_non_streaming(monkeypatch):
return FakeResp()
async def fake_client(*args, **kwargs):
def fake_client(*args, **kwargs):
class Ctx:
async def __aenter__(self2): return self2
async def __aexit__(*a): pass
@@ -168,7 +168,8 @@ def test_stream_ollama_text_deltas(monkeypatch):
])
class LineIterator:
async def __anext__(self):
def __aiter__(self2): return self2
async def __anext__(self2):
try:
return next(lines_iter)
except StopIteration:
@@ -177,8 +178,8 @@ def test_stream_ollama_text_deltas(monkeypatch):
class Response:
def __init__(self2): self2._lines = LineIterator()
async def raise_for_status(self2): pass
async def aiter_lines(self2): return self2._lines
def raise_for_status(self2): pass
def aiter_lines(self2): return self2._lines
class StreamCtx:
async def __aenter__(self2): return Response()
@@ -186,10 +187,12 @@ def test_stream_ollama_text_deltas(monkeypatch):
class Client:
stream = lambda self2, *args, **kw: StreamCtx()
async def __aenter__(self2): return self2
async def __aexit__(*a): pass
return Client()
async def fake_client(*args, **kwargs):
def fake_client(*args, **kwargs):
captured["called"] = True
return make_lines()
@@ -217,7 +220,8 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
])
class LineIterator:
async def __anext__(self):
def __aiter__(self2): return self2
async def __anext__(self2):
try:
return next(lines_iter)
except StopIteration:
@@ -226,8 +230,8 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
class Response:
def __init__(self2): self2._lines = LineIterator()
async def raise_for_status(self2): pass
async def aiter_lines(self2): return self2._lines
def raise_for_status(self2): pass
def aiter_lines(self2): return self2._lines
class StreamCtx:
async def __aenter__(self2): return Response()
@@ -235,10 +239,12 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
class Client:
stream = lambda self2, *args, **kw: StreamCtx()
async def __aenter__(self2): return self2
async def __aexit__(*a): pass
return Client()
async def fake_client(*args, **kwargs):
def fake_client(*args, **kwargs):
captured["called"] = True
return make_lines()
@@ -260,9 +266,9 @@ def test_stream_ollama_events_thinking_and_content(monkeypatch):
def test_call_vlm_ocr(monkeypatch):
captured = {}
async def fake_post(url, json=None):
captured["url"] = url
captured["json"] = json
async def fake_post(*args, **kwargs):
captured["url"] = args[1] if len(args) > 1 else kwargs.get("url", "")
captured["json"] = kwargs.get("json")
class FakeResp:
def raise_for_status(self): pass
@@ -270,7 +276,7 @@ def test_call_vlm_ocr(monkeypatch):
return FakeResp()
async def fake_client(*args, **kwargs):
def fake_client(*args, **kwargs):
class Ctx:
async def __aenter__(self2): return self2
async def __aexit__(*a): pass
+1 -1
View File
@@ -138,7 +138,7 @@ def test_post_completions_privacy_mode(monkeypatch):
captured["kwargs"] = kwargs
return {"content": "done", "think": ""}
monkeypatch.setattr(main, "call_ollama", fake_call)
monkeypatch.setattr(main, "build_completion_prompts", lambda *a, **k: ("sys", "user"))
monkeypatch.setattr(main, "build_completion_prompts", lambda *a, **k: ("sys", "user", ""))
monkeypatch.setattr(main, "prepare_prompt_context", lambda *a, **k: ("p", "s"))
client = TestClient(main.app)
+9 -6
View File
@@ -56,19 +56,22 @@ def test_pro_status_missing_returns_404():
assert response.status_code == 404
def test_pro_prompt_uses_simple_chat_instruction():
def test_pro_prompt_uses_pro_specific_instruction():
system_prompt, user_prompt = pro_completions._build_pro_prompts(
prefix="欢迎使用 LLM-IN-TEXT\n\n即时可用的 LLM 系统",
suffix="",
language_id="markdown",
instruction="",
pro_thinking="high",
)
combined = f"{system_prompt}\n{user_prompt}".lower()
assert "pro block" not in combined
assert "replacement" not in combined
assert "final answer" not in combined
assert "markdown before cursor" in combined
assert "markdown after cursor" in combined
assert "[pro] model for llm-in-text" in combined
assert "pro_mode: true" in combined
assert "pro_thinking_level: high" in combined
assert "long paragraphs or section-level output are allowed" in combined
assert "highest priority" in combined
assert "never copy tags to output" in combined
assert "write only the markdown that belongs at the cursor" not in combined
assert "continue the markdown naturally" in combined