feat: implement inline autocomplete suggestions with FastAPI backend and Milkdown editor integration

This commit is contained in:
2026-01-18 19:42:58 +08:00
parent ba49f82953
commit 6102f905ee
10 changed files with 633 additions and 52 deletions
+3
View File
@@ -0,0 +1,3 @@
OPENAI_API_KEY=ollama
OLLAMA_BASE_URL=http://192.168.0.120:11434/v1/
OLLAMA_MODEL=gpt-oss:120b
+3
View File
@@ -0,0 +1,3 @@
OPENAI_API_KEY=ollama
OLLAMA_BASE_URL=http://192.168.0.120:11434/v1/
OLLAMA_MODEL=gpt-oss:120b
+44
View File
@@ -0,0 +1,44 @@
import os
from typing import AsyncGenerator
from openai import AsyncOpenAI
import json
api_key = os.getenv('OPENAI_API_KEY', 'ollama')
base_url = os.getenv('OLLAMA_BASE_URL', 'http://192.168.0.120:11434/v1/')
model = os.getenv('OLLAMA_MODEL', 'gpt-oss:120b')
print(f"[LLM] API key configured: {'Yes' if api_key else 'No'}")
print(f"[LLM] Base URL: {base_url}")
print(f"[LLM] Model: {model}")
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
async def stream_openai(prompt: str) -> AsyncGenerator[str, None]:
"""
调用 OpenAI/Ollama API 并流式返回补全内容。
参考 completions-sample-code 的 streaming 逻辑。
"""
print(f"[LLM] Calling API with prompt length: {len(prompt)}")
try:
stream = await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=True,
max_tokens=128,
temperature=0.2,
)
chunk_count = 0
async for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
chunk_count += 1
print(f"[LLM] Chunk {chunk_count}: {content}")
yield json.dumps({"content": content})
print(f"[LLM] Stream complete, total chunks: {chunk_count}")
except Exception as e:
error_msg = f"Error: {str(e)}"
print(f"[LLM] Error: {error_msg}")
yield json.dumps({"error": str(e)})
+47
View File
@@ -0,0 +1,47 @@
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import os
import json
app = FastAPI()
class CompletionRequest(BaseModel):
prefix: str
suffix: str
languageId: str = 'markdown'
def generate_stream(request: CompletionRequest):
from prompt import build_prompt
from llm import stream_openai
print(f"[Backend] Received request - prefix length: {len(request.prefix)}, suffix length: {len(request.suffix)}")
try:
prompt = build_prompt(request.prefix, request.suffix)
print(f"[Backend] Built prompt (first 100 chars): {prompt[:100]}...")
async def gen():
chunk_count = 0
async for chunk in stream_openai(prompt):
chunk_count += 1
yield f"data: {chunk}\n\n"
if chunk_count % 5 == 0:
print(f"[Backend] Sent chunk {chunk_count}")
yield "data: {\"done\": true}\n\n"
print(f"[Backend] Stream complete, total chunks: {chunk_count}")
return gen()
except Exception as e:
error_msg = f"{{\"error\": \"{str(e)}\"}}"
print(f"[Backend] Error: {e}")
yield f"data: {error_msg}\n\n"
@app.post("/v1/completions")
async def create_completion(request: CompletionRequest):
print(f"[Backend] POST /v1/completions called")
return StreamingResponse(generate_stream(request), media_type="text/event-stream")
if __name__ == "__main__":
import uvicorn
print("[Backend] Starting server on http://0.0.0.0:8000")
uvicorn.run(app, host="0.0.0.0", port=8000)
+28
View File
@@ -0,0 +1,28 @@
import os
from typing import Tuple
def build_prompt(prefix: str, suffix: str) -> str:
"""
构建用于代码补全的 Prompt。
参考 completions-sample-code 的 extractPrompt 逻辑简化实现。
"""
MAX_CONTEXT_LINES = 30
prefix_lines = prefix.split('\n')
suffix_lines = suffix.split('\n') if suffix else []
recent_prefix = '\n'.join(prefix_lines[-MAX_CONTEXT_LINES:])
recent_suffix = '\n'.join(suffix_lines[:5])
prompt = f"""
You are a helpful writing assistant. Continue the text naturally based on the context.
Context (before cursor):
{recent_prefix}
Complete this:
{suffix if suffix else '(cursor here)'}
Continue:"""
return prompt.strip()
+5
View File
@@ -0,0 +1,5 @@
fastapi
uvicorn
openai
pydantic
python-dotenv