feat: add fission-aware ai prompts
This commit is contained in:
@@ -142,26 +142,18 @@ func (s *Server) handleAIAsk(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Question string `json:"question"`
|
||||
}
|
||||
var req aiPromptRequest
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 4*1024)).Decode(&req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Question) == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "question is required")
|
||||
chatReq, err := buildLLMChatRequest(req)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"model": "gpt-oss-120b",
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": "Ты умный ассистент. Отвечай кратко и по делу."},
|
||||
{"role": "user", "content": req.Question},
|
||||
},
|
||||
"max_tokens": 1024,
|
||||
})
|
||||
body, _ := json.Marshal(chatReq)
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
aiModeChat = "chat"
|
||||
aiModeCodegen = "codegen"
|
||||
aiModeExplain = "explain"
|
||||
)
|
||||
|
||||
type llmMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type aiPromptRequest struct {
|
||||
Mode string `json:"mode"`
|
||||
Language string `json:"language"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Code string `json:"code"`
|
||||
Question string `json:"question"`
|
||||
}
|
||||
|
||||
type llmChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []llmMessage `json:"messages"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
}
|
||||
|
||||
func buildLLMChatRequest(req aiPromptRequest) (llmChatRequest, error) {
|
||||
mode := normalizeAIMode(req.Mode)
|
||||
lang := normalizeLanguage(req.Language)
|
||||
|
||||
switch mode {
|
||||
case aiModeCodegen:
|
||||
if lang == "" {
|
||||
return llmChatRequest{}, fmt.Errorf("language is required")
|
||||
}
|
||||
return llmChatRequest{
|
||||
Model: "gpt-oss-120b",
|
||||
Messages: []llmMessage{
|
||||
{Role: "system", Content: buildCodegenSystemPrompt(lang)},
|
||||
{Role: "user", Content: buildCodegenUserPrompt(lang, req.Name, req.Description)},
|
||||
},
|
||||
MaxTokens: 1024,
|
||||
}, nil
|
||||
case aiModeExplain:
|
||||
if lang == "" {
|
||||
return llmChatRequest{}, fmt.Errorf("language is required")
|
||||
}
|
||||
if strings.TrimSpace(req.Code) == "" {
|
||||
return llmChatRequest{}, fmt.Errorf("code is required")
|
||||
}
|
||||
return llmChatRequest{
|
||||
Model: "gpt-oss-120b",
|
||||
Messages: []llmMessage{
|
||||
{Role: "system", Content: buildExplainSystemPrompt(lang)},
|
||||
{Role: "user", Content: buildExplainUserPrompt(lang, req.Code)},
|
||||
},
|
||||
MaxTokens: 1024,
|
||||
}, nil
|
||||
default:
|
||||
question := strings.TrimSpace(req.Question)
|
||||
if question == "" {
|
||||
return llmChatRequest{}, fmt.Errorf("question is required")
|
||||
}
|
||||
return llmChatRequest{
|
||||
Model: "gpt-oss-120b",
|
||||
Messages: []llmMessage{
|
||||
{Role: "system", Content: buildChatSystemPrompt()},
|
||||
{Role: "user", Content: question},
|
||||
},
|
||||
MaxTokens: 1024,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAIMode(mode string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case aiModeCodegen:
|
||||
return aiModeCodegen
|
||||
case aiModeExplain:
|
||||
return aiModeExplain
|
||||
default:
|
||||
return aiModeChat
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeLanguage(language string) string {
|
||||
return strings.ToLower(strings.TrimSpace(language))
|
||||
}
|
||||
|
||||
func buildCodegenSystemPrompt(lang string) string {
|
||||
switch lang {
|
||||
case "python":
|
||||
return strings.TrimSpace(`Ты генератор кода для Fission Console.
|
||||
Возвращай ТОЛЬКО код, без markdown, без пояснений, без fenced-блоков.
|
||||
Генерируй только Fission-usable Python code.
|
||||
Канон для Python: файл main.py, entrypoint main.main, функция def main(): без аргументов.
|
||||
Не генерируй def main(ctx), def handler, Flask-приложение или обычный CLI-main.
|
||||
Если нужен ответ HTTP-подобного вида, возвращай dict со status/body/headers или строку; не выдумывай лишние зависимости.
|
||||
Если описание пользователя расплывчатое, выбери минимальную рабочую Fission-функцию.`)
|
||||
|
||||
case "nodejs":
|
||||
return strings.TrimSpace(`Ты генератор кода для Fission Console.
|
||||
Возвращай ТОЛЬКО код, без markdown, без пояснений, без fenced-блоков.
|
||||
Генерируй только Fission-usable Node.js code.
|
||||
Канон для Node.js: user code должен быть CommonJS-style, обычно module.exports = async function (context) { ... }.
|
||||
Не используй ESM import/export в user code. Не пиши обычный CLI main.
|
||||
Возвращай объект вида { status, body, headers } или строку/JSON, совместимые с Fission node wrapper.
|
||||
Если описание пользователя расплывчатое, выбери минимальную рабочую Fission-функцию.`)
|
||||
|
||||
case "go":
|
||||
return strings.TrimSpace(`Ты генератор кода для Fission Console.
|
||||
Возвращай ТОЛЬКО код, без markdown, без пояснений, без fenced-блоков.
|
||||
Генерируй только Fission-usable Go code.
|
||||
Канон для Go: package main, экспортированная функция func Handler(w http.ResponseWriter, r *http.Request).
|
||||
Не генерируй func main() как entrypoint и не делай CLI-программу.
|
||||
Импортируй net/http и другие пакеты только если они реально нужны.
|
||||
Если описание пользователя расплывчатое, выбери минимальную рабочую Fission-функцию.`)
|
||||
|
||||
case "php":
|
||||
return strings.TrimSpace(`Ты генератор кода для Fission Console.
|
||||
Возвращай ТОЛЬКО код, без markdown, без пояснений, без fenced-блоков.
|
||||
Генерируй только Fission-usable PHP code.
|
||||
Канон для PHP: файл main.php, entrypoint main.php::handler, функция function handler($context).
|
||||
Не пиши CLI-скрипт и не выдумывай другой entrypoint.
|
||||
Для ответа используй $context["response"] или совместимый Fission response pattern.
|
||||
Если описание пользователя расплывчатое, выбери минимальную рабочую Fission-функцию.`)
|
||||
|
||||
case "ruby":
|
||||
return strings.TrimSpace(`Ты генератор кода для Fission Console.
|
||||
Возвращай ТОЛЬКО код, без markdown, без пояснений, без fenced-блоков.
|
||||
Генерируй только Fission-usable Ruby code.
|
||||
Канон для Ruby: файл handler.rb, entrypoint handler, функция def handler.
|
||||
Не пиши CLI-скрипт и не выдумывай другой entrypoint.
|
||||
Если описание пользователя расплывчатое, выбери минимальную рабочую Fission-функцию.`)
|
||||
|
||||
default:
|
||||
return strings.TrimSpace(`Ты генератор кода для Fission Console.
|
||||
Возвращай ТОЛЬКО код, без markdown, без пояснений, без fenced-блоков.
|
||||
Генерируй только Fission-usable code.
|
||||
Если язык не распознан, выбери минимальную рабочую реализацию без лишней обвязки.
|
||||
Если описание пользователя расплывчатое, выбери минимальную рабочую Fission-функцию.`)
|
||||
}
|
||||
}
|
||||
|
||||
func buildCodegenUserPrompt(lang, name, description string) string {
|
||||
funcName := strings.TrimSpace(name)
|
||||
if funcName == "" {
|
||||
funcName = defaultPromptFunctionName(lang)
|
||||
}
|
||||
goal := strings.TrimSpace(description)
|
||||
if goal == "" {
|
||||
goal = "Сгенерируй минимальную рабочую Fission-функцию."
|
||||
}
|
||||
|
||||
return strings.TrimSpace(fmt.Sprintf(`Сгенерируй функцию для Fission.
|
||||
Language: %s
|
||||
Function name: %s
|
||||
What to do: %s
|
||||
|
||||
Правила:
|
||||
1) Верни только код.
|
||||
2) Соблюдай канон Fission для этого языка.
|
||||
3) Если пользователь описал задачу плохо или противоречиво, выбери самый безопасный и минимальный рабочий вариант.
|
||||
4) Не добавляй markdown, пояснений и лишних файлов.
|
||||
5) Код должен быть пригоден для вставки в console и запуска в Fission без ручной правки entrypoint.`, lang, funcName, goal))
|
||||
}
|
||||
|
||||
func buildExplainSystemPrompt(lang string) string {
|
||||
return strings.TrimSpace(fmt.Sprintf(`Ты анализатор кода для Fission Console.
|
||||
Отвечай кратко, по делу и без markdown-блоков.
|
||||
Сначала объясни, что делает код.
|
||||
Затем отдельно скажи, подходит ли он для Fission %s и почему.
|
||||
Если entrypoint, сигнатура или return shape не совпадают с каноном Fission, скажи это прямо.
|
||||
Не переписывай код и не выдумывай поведение, которого нет.`, prettyLanguageName(lang)))
|
||||
}
|
||||
|
||||
func buildExplainUserPrompt(lang, code string) string {
|
||||
return strings.TrimSpace(fmt.Sprintf(`Объясни этот %s код и проверь его на совместимость с Fission.
|
||||
|
||||
Требования к ответу:
|
||||
1) Что делает код.
|
||||
2) Совместим ли он с Fission.
|
||||
3) Если не совместим — что именно сломано (entrypoint, сигнатура, return, экспорт, файл).
|
||||
|
||||
Код:
|
||||
%s`, prettyLanguageName(lang), strings.TrimSpace(code)))
|
||||
}
|
||||
|
||||
func buildChatSystemPrompt() string {
|
||||
return "Ты краткий и полезный ассистент для Fission Console. Отвечай по делу и без лишней болтовни."
|
||||
}
|
||||
|
||||
func prettyLanguageName(lang string) string {
|
||||
switch normalizeLanguage(lang) {
|
||||
case "nodejs":
|
||||
return "Node.js"
|
||||
case "python":
|
||||
return "Python"
|
||||
case "go":
|
||||
return "Go"
|
||||
case "php":
|
||||
return "PHP"
|
||||
case "ruby":
|
||||
return "Ruby"
|
||||
default:
|
||||
return "этот язык"
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPromptFunctionName(lang string) string {
|
||||
switch normalizeLanguage(lang) {
|
||||
case "go":
|
||||
return "Handler"
|
||||
case "php":
|
||||
return "handler"
|
||||
case "ruby":
|
||||
return "handler"
|
||||
case "nodejs":
|
||||
return "main"
|
||||
default:
|
||||
return "main"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeAIMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{name: "blank defaults to chat", in: "", want: aiModeChat},
|
||||
{name: "case-insensitive codegen", in: " CODEGEN ", want: aiModeCodegen},
|
||||
{name: "case-insensitive explain", in: "Explain", want: aiModeExplain},
|
||||
{name: "unknown defaults to chat", in: "boom", want: aiModeChat},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizeAIMode(tt.in); got != tt.want {
|
||||
t.Fatalf("normalizeAIMode(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultPromptFunctionName(t *testing.T) {
|
||||
tests := []struct {
|
||||
lang string
|
||||
want string
|
||||
}{
|
||||
{lang: "python", want: "main"},
|
||||
{lang: "nodejs", want: "main"},
|
||||
{lang: "go", want: "Handler"},
|
||||
{lang: "php", want: "handler"},
|
||||
{lang: "ruby", want: "handler"},
|
||||
{lang: "unknown", want: "main"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.lang, func(t *testing.T) {
|
||||
if got := defaultPromptFunctionName(tt.lang); got != tt.want {
|
||||
t.Fatalf("defaultPromptFunctionName(%q) = %q, want %q", tt.lang, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestCodegenPython(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||||
Mode: aiModeCodegen,
|
||||
Language: "python",
|
||||
Name: " ",
|
||||
Description: " сделать простой echo endpoint ",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
if req.Model != "gpt-oss-120b" {
|
||||
t.Fatalf("unexpected model %q", req.Model)
|
||||
}
|
||||
if got, want := len(req.Messages), 2; got != want {
|
||||
t.Fatalf("messages len = %d, want %d", got, want)
|
||||
}
|
||||
sys := req.Messages[0].Content
|
||||
usr := req.Messages[1].Content
|
||||
if !strings.Contains(sys, "Fission") || !strings.Contains(sys, "main.main") || !strings.Contains(sys, "def main():") {
|
||||
t.Fatalf("python system prompt is missing Fission rules: %s", sys)
|
||||
}
|
||||
if !strings.Contains(sys, "не генерируй def main(ctx)") && !strings.Contains(strings.ToLower(sys), "def main(ctx)") {
|
||||
t.Fatalf("python system prompt must forbid ctx main: %s", sys)
|
||||
}
|
||||
if !strings.Contains(usr, "Language: python") {
|
||||
t.Fatalf("python user prompt missing language: %s", usr)
|
||||
}
|
||||
if !strings.Contains(usr, "Function name: main") {
|
||||
t.Fatalf("blank name should fall back to main: %s", usr)
|
||||
}
|
||||
if !strings.Contains(usr, "echo endpoint") {
|
||||
t.Fatalf("description must be present: %s", usr)
|
||||
}
|
||||
if !strings.Contains(usr, "Верни только код") || !strings.Contains(usr, "entrypoint") {
|
||||
t.Fatalf("python user prompt missing constraints: %s", usr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestCodegenNodeJS(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||||
Mode: aiModeCodegen,
|
||||
Language: "nodejs",
|
||||
Name: "handler",
|
||||
Description: "возвращает status/body и не падает на пустом input",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
sys := req.Messages[0].Content
|
||||
usr := req.Messages[1].Content
|
||||
for _, want := range []string{"CommonJS", "module.exports", "Fission node wrapper", "status, body, headers"} {
|
||||
if !strings.Contains(sys, want) {
|
||||
t.Fatalf("node system prompt missing %q: %s", want, sys)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(usr, "Language: nodejs") || !strings.Contains(usr, "Function name: handler") {
|
||||
t.Fatalf("node user prompt missing fields: %s", usr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestCodegenGo(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||||
Mode: aiModeCodegen,
|
||||
Language: "go",
|
||||
Description: "печатает ответ и ставит content-type",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
sys := req.Messages[0].Content
|
||||
usr := req.Messages[1].Content
|
||||
for _, want := range []string{"package main", "func Handler(w http.ResponseWriter, r *http.Request)", "net/http"} {
|
||||
if !strings.Contains(strings.ToLower(sys), strings.ToLower(want)) {
|
||||
t.Fatalf("go system prompt missing %q: %s", want, sys)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(usr, "Function name: Handler") {
|
||||
t.Fatalf("go user prompt must default to Handler: %s", usr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestCodegenPHP(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeCodegen, Language: "php", Description: "simple greeting"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
sys := req.Messages[0].Content
|
||||
for _, want := range []string{"main.php", "main.php::handler", "function handler($context)", "$context[\"response\"]"} {
|
||||
if !strings.Contains(sys, want) {
|
||||
t.Fatalf("php system prompt missing %q: %s", want, sys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestCodegenRuby(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeCodegen, Language: "ruby", Description: "return hello"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
sys := req.Messages[0].Content
|
||||
for _, want := range []string{"handler.rb", "entrypoint handler", "def handler"} {
|
||||
if !strings.Contains(sys, want) {
|
||||
t.Fatalf("ruby system prompt missing %q: %s", want, sys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestExplain(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||||
Mode: aiModeExplain,
|
||||
Language: "python",
|
||||
Code: "def main():\n return 'ok'\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
sys := req.Messages[0].Content
|
||||
usr := req.Messages[1].Content
|
||||
for _, want := range []string{"объясни", "совместим ли он с Fission", "Если entrypoint", "не переписывай код"} {
|
||||
if !strings.Contains(strings.ToLower(sys+"\n"+usr), strings.ToLower(want)) {
|
||||
t.Fatalf("explain prompt missing %q: sys=%s usr=%s", want, sys, usr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestChat(t *testing.T) {
|
||||
req, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeChat, Question: "что делает слой 2?"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||||
}
|
||||
if len(req.Messages) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(req.Messages))
|
||||
}
|
||||
if req.Messages[1].Content != "что делает слой 2?" {
|
||||
t.Fatalf("chat question mismatch: %q", req.Messages[1].Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLLMChatRequestRejectsMissingFields(t *testing.T) {
|
||||
if _, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeCodegen, Description: "x"}); err == nil {
|
||||
t.Fatal("expected codegen without language to fail")
|
||||
}
|
||||
if _, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeExplain, Language: "python"}); err == nil {
|
||||
t.Fatal("expected explain without code to fail")
|
||||
}
|
||||
if _, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeChat}); err == nil {
|
||||
t.Fatal("expected chat without question to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAIAskUsesCodegenPrompt(t *testing.T) {
|
||||
var gotReq llmChatRequest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/chat/completions" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
||||
t.Fatalf("decode upstream request: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"module.exports = async function () { return { status: 200, body: \"ok\" }; }"}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
s := &Server{http: server.Client(), llmURL: server.URL, llmKey: "secret"}
|
||||
req := httptest.NewRequest(http.MethodPost, "/console/api/ai/ask", bytes.NewBufferString(`{"mode":"codegen","language":"python","description":"сделай echo"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleAIAsk(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if gotReq.Model != "gpt-oss-120b" {
|
||||
t.Fatalf("model = %q", gotReq.Model)
|
||||
}
|
||||
if len(gotReq.Messages) != 2 {
|
||||
t.Fatalf("messages len = %d", len(gotReq.Messages))
|
||||
}
|
||||
if !strings.Contains(gotReq.Messages[0].Content, "Python") || !strings.Contains(gotReq.Messages[1].Content, "echo") {
|
||||
t.Fatalf("unexpected upstream prompts: %#v", gotReq.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAIAskUsesExplainPrompt(t *testing.T) {
|
||||
var gotReq llmChatRequest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
||||
t.Fatalf("decode upstream request: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"код делает hello"}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
s := &Server{http: server.Client(), llmURL: server.URL, llmKey: "secret"}
|
||||
req := httptest.NewRequest(http.MethodPost, "/console/api/ai/ask", bytes.NewBufferString(`{"mode":"explain","language":"go","code":"package main\nfunc Handler(w http.ResponseWriter, r *http.Request) {}"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleAIAsk(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(gotReq.Messages[0].Content, "Go") || !strings.Contains(gotReq.Messages[1].Content, "совместим") {
|
||||
t.Fatalf("unexpected explain prompts: %#v", gotReq.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAIAskKeepsChatModeSimple(t *testing.T) {
|
||||
var gotReq llmChatRequest
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
||||
t.Fatalf("decode upstream request: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
s := &Server{http: server.Client(), llmURL: server.URL, llmKey: "secret"}
|
||||
req := httptest.NewRequest(http.MethodPost, "/console/api/ai/ask", bytes.NewBufferString(`{"question":"что такое слой 1?"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
s.handleAIAsk(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if gotReq.Messages[1].Content != "что такое слой 1?" {
|
||||
t.Fatalf("chat question mismatch: %#v", gotReq.Messages)
|
||||
}
|
||||
}
|
||||
+12
-5
@@ -1405,7 +1405,7 @@
|
||||
var r = await fetch('/console/api/ai/ask', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify({ question: q })
|
||||
body: JSON.stringify({ mode: 'chat', question: q })
|
||||
});
|
||||
var d = await r.json();
|
||||
msgs.textContent = msgs.textContent.replace('⏳ ...', '🤖 ' + (d.answer || d.error || 'Нет ответа'));
|
||||
@@ -1430,8 +1430,12 @@
|
||||
resEl.style.display = 'block'; resEl.style.background = 'var(--bg-alt)'; resEl.style.color = 'var(--fg)';
|
||||
resEl.textContent = 'Запрашиваю у LLM...';
|
||||
try {
|
||||
var q = 'Напиши функцию Fission на ' + lang + ' с именем "' + name + '". Функция должна: ' + desc + '. Верни только чистый код без пояснений и без markdown-блоков.';
|
||||
var d = await requestJSON('/console/api/ai/ask', 'POST', { question: q });
|
||||
var d = await requestJSON('/console/api/ai/ask', 'POST', {
|
||||
mode: 'codegen',
|
||||
language: lang,
|
||||
name: name,
|
||||
description: desc
|
||||
});
|
||||
var code = (d.answer || '').replace(/^```[\w]*\n?/, '').replace(/\n?```$/, '');
|
||||
document.getElementById('c-code').value = addLLMWarning(code, lang);
|
||||
document.getElementById('c-gen-prompt').style.display = 'none';
|
||||
@@ -1455,8 +1459,11 @@
|
||||
resEl.style.display = 'block'; resEl.style.background = 'var(--bg-alt)'; resEl.style.color = 'var(--fg)';
|
||||
resEl.textContent = 'Запрашиваю у LLM...';
|
||||
try {
|
||||
var q = 'Кратко объясни что делает этот ' + lang + ' код:\n' + code;
|
||||
var d = await requestJSON('/console/api/ai/ask', 'POST', { question: q });
|
||||
var d = await requestJSON('/console/api/ai/ask', 'POST', {
|
||||
mode: 'explain',
|
||||
language: lang,
|
||||
code: code
|
||||
});
|
||||
resEl.style.background = '#1a2a3a'; resEl.style.color = '#8cf';
|
||||
resEl.textContent = d.answer || '(пустой ответ)';
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user