From 2c7a02f587b0ad60ecf111f74cda28316f3a175b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cydy0615=E2=80=9D?= <“allenyuan410@gmail.com”> Date: Tue, 2 Jun 2026 21:23:34 +0800 Subject: [PATCH] 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. --- backend/llm.py | 28 ++++-- backend/main.py | 6 +- backend/pro_completions.py | 47 +++------- backend/prompt.py | 107 ++++++++++++++++++++++- backend/prompts/__init__.py | 8 ++ backend/prompts/inline_examples.json | 2 +- backend/prompts/inline_examples_pro.json | 3 + backend/prompts/system_prompt.json | 2 +- backend/prompts/system_prompt_pro.json | 3 + backend/tests/test_llm.py | 38 ++++---- backend/tests/test_main_endpoints.py | 2 +- backend/tests/test_pro_completions.py | 15 ++-- 12 files changed, 191 insertions(+), 70 deletions(-) create mode 100644 backend/prompts/inline_examples_pro.json create mode 100644 backend/prompts/system_prompt_pro.json diff --git a/backend/llm.py b/backend/llm.py index c69cf24..f2a6369 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -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, ) diff --git a/backend/main.py b/backend/main.py index ad2eb71..0d0480c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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))) diff --git a/backend/pro_completions.py b/backend/pro_completions.py index d43cd1a..5e72adc 100644 --- a/backend/pro_completions.py +++ b/backend/pro_completions.py @@ -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, ) diff --git a/backend/prompt.py b/backend/prompt.py index 9d9c2a7..a22c3ce 100644 --- a/backend/prompt.py +++ b/backend/prompt.py @@ -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 === + + +{recent_prefix} + + + +{recent_suffix} + + +Output:""" + + system_prompt = build_pro_system_prompt(safe_language_id) + return system_prompt.strip(), user_prompt.strip() diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py index 8c1b9ba..c5b39d4 100644 --- a/backend/prompts/__init__.py +++ b/backend/prompts/__init__.py @@ -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", "") diff --git a/backend/prompts/inline_examples.json b/backend/prompts/inline_examples.json index da4e9aa..7f13fae 100644 --- a/backend/prompts/inline_examples.json +++ b/backend/prompts/inline_examples.json @@ -1,3 +1,3 @@ { - "content": "=== CATEGORY A: PROSE CONTINUATION ===\n\n[EX01] Simple prose continuation\nThe quick brown fox \njumps over the lazy dog.\nExpected OUTPUT:\nmoved quietly and then\n\n[EX02] Avoid repeating suffix\nOur launch plan starts with \nphase one, followed by phase two.\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## TODO\n- [ ] Buy milk\n- [ ] \n\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\nDeployment steps:\n\nExpected OUTPUT:\n\n- Build artifact\n- Deploy service\n\n[EX05] Continue table row\n| Name | Score |\n| --- | --- |\n| Alice | 92 |\n| Bob | \n\nExpected OUTPUT:\n88 |\n\n[EX06] Start new paragraph\nFirst paragraph ends.\n\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\nEnd of previous section.\n## Next Heading\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\nParse this JSON payload in Python:\n\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```python\ndef add(a, b):\nreturn \n\n```\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```python\ndef hello():\n\n```\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\nThe derivative of x^2 is \n.\nExpected OUTPUT:\n$2x$\nWRONG: 2x (bare formula)\n\n[EX12] Block math\nWe can write the Gaussian integral as:\n\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```mermaid\nflowchart TD\nA[Start] --> \n\n```\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\nPlease provide a simple release pipeline diagram.\n\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![whiteboard](img.png) \nThe relationship is \n.\nExpected OUTPUT:\n$y = mx + b$\nWRONG: (OCR tag in output)" + "content": "=== CATEGORY A: PROSE CONTINUATION ===\n\n[EX01] Simple prose continuation\nThe quick brown fox \njumps over the lazy dog.\nExpected OUTPUT:\nmoved quietly and then\n\n[EX02] Avoid repeating suffix\nOur launch plan starts with \nphase one, followed by phase two.\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## TODO\n- [ ] Buy milk\n- [ ] \n\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\nDeployment steps:\n\nExpected OUTPUT:\n\n- Build artifact\n- Deploy service\n\n[EX05] Continue table row\n| Name | Score |\n| --- | --- |\n| Alice | 92 |\n| Bob | \n\nExpected OUTPUT:\n88 |\n\n[EX06] Start new paragraph\nFirst paragraph ends.\n\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\nEnd of previous section.\n## Next Heading\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\nParse this JSON payload in Python:\n\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```python\ndef add(a, b):\n return \n\n```\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```python\ndef hello():\n\n```\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\nThe derivative of x^2 is \n.\nExpected OUTPUT:\n$2x$\nWRONG: 2x (bare formula)\n\n[EX12] Block math\nWe can write the Gaussian integral as:\n\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```mermaid\nflowchart TD\n A[Start] --> \n\n```\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\nPlease provide a simple release pipeline diagram.\n\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![whiteboard](img.png) \nThe relationship is \n.\nExpected OUTPUT:\n$y = mx + b$\nWRONG: (OCR tag in output)" } diff --git a/backend/prompts/inline_examples_pro.json b/backend/prompts/inline_examples_pro.json new file mode 100644 index 0000000..410b399 --- /dev/null +++ b/backend/prompts/inline_examples_pro.json @@ -0,0 +1,3 @@ +{ + "content": "=== PRO CATEGORY A: CONTINUATION AND EXPANSION ===\n\n[PRO-EX01] Continue naturally without suffix repetition\nProject update: This week we completed \nand started preparing the release checklist.\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.\nOur migration reduced incidents.\n\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## Launch Plan\nCurrent status is green.\n\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\n1. Prepare schema\n2. Run dry-run\n3. \n\nExpected OUTPUT:\nValidate production metrics and sign off\n\n[PRO-EX05] Keep table shape valid\n| Metric | Before | After |\n| --- | --- | --- |\n| P95 latency | 420ms | \n\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```python\ndef build_payload(user_id):\n return \n\n```\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.\nFetch active users:\n\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\nThe expected value is \n under this distribution.\nExpected OUTPUT:\n$\\mu$\n\n[PRO-EX09] Mermaid inside mermaid fence\nCURSOR_FENCE_LANGUAGE=mermaid\nCURSOR_IN_FENCED_CODE_BLOCK=true\n```mermaid\nflowchart TD\nA[Input] --> \n\n```\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\nShow the pipeline as a diagram.\n\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这个方案看起来不错,但是细节很多,可能会拖慢推进。\n\nExpected OUTPUT:\n该方案方向正确,但需聚焦关键路径并压缩实现范围,以保障交付节奏。\n\n[PRO-EX12] Add constrained output length\nINSTRUCTION: Add one sentence under 25 words.\n结论:\n\nExpected OUTPUT:\n\n先完成最小可用版本,再按风险优先级迭代。\n\n=== PRO CATEGORY F: HIDDEN CONTEXT SAFETY ===\n\n[PRO-EX13] Never leak OCR tags\n![board](a.png) \nTimeline:\n\nExpected OUTPUT:\n\nQ3 launch with weekly checkpoints and ownership tracking.\nWRONG: \n\n=== PRO CATEGORY G: BOUNDARY PRECISION ===\n\n[PRO-EX14] Add leading newline when needed\nPREFIX_ENDS_WITH_NEWLINE=false\nAction items:\n\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\nSummary complete.\n## Next Steps\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.\nDeployment guide draft:\n\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```" +} diff --git a/backend/prompts/system_prompt.json b/backend/prompts/system_prompt.json index ecc7391..31abe5d 100644 --- a/backend/prompts/system_prompt.json +++ b/backend/prompts/system_prompt.json @@ -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 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 is hidden context\n- Never copy OCR tags to output\n- Use OCR content as semantic hint only" } diff --git a/backend/prompts/system_prompt_pro.json b/backend/prompts/system_prompt_pro.json new file mode 100644 index 0000000..f66cbed --- /dev/null +++ b/backend/prompts/system_prompt_pro.json @@ -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., ) 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." +} diff --git a/backend/tests/test_llm.py b/backend/tests/test_llm.py index 5b35a60..bdc6cfa 100644 --- a/backend/tests/test_llm.py +++ b/backend/tests/test_llm.py @@ -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 diff --git a/backend/tests/test_main_endpoints.py b/backend/tests/test_main_endpoints.py index e925faf..fe63d75 100644 --- a/backend/tests/test_main_endpoints.py +++ b/backend/tests/test_main_endpoints.py @@ -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) diff --git a/backend/tests/test_pro_completions.py b/backend/tests/test_pro_completions.py index 872feee..a5b8c85 100644 --- a/backend/tests/test_pro_completions.py +++ b/backend/tests/test_pro_completions.py @@ -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