prompt-rewriter.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "strings"
  9. "time"
  10. "github.com/maximhq/bifrost/core/schemas"
  11. )
  12. // Config that can be set in Bifrost's config.json
  13. type PluginConfig struct {
  14. SmallModelEndpoint string `json:"small_model_endpoint"` // e.g. "http://localhost:8081/v1/chat/completions"
  15. SmallModelName string `json:"small_model_name"` // e.g. "qwen3-4b-instruct"
  16. RewriteSystemPrompt string `json:"rewrite_system_prompt"`
  17. EnableLogging bool `json:"enable_logging"`
  18. TimeoutSeconds int `json:"timeout_seconds"`
  19. }
  20. var config PluginConfig
  21. func Init(cfg any) error {
  22. // Default values
  23. config = PluginConfig{
  24. SmallModelEndpoint: "https://bifrost.home.timandjenni.com/openai/v1/chat/completions", // change to your llama.cpp / ollama / lemonade endpoint
  25. SmallModelName: "qwen3-4b-instruct",
  26. RewriteSystemPrompt: `You are an expert prompt engineer. Rewrite the user's raw request into a clear, well-structured, high-quality prompt that will produce better results from a larger coding/reasoning model.
  27. Keep the original intent. Make it more precise, add useful structure if helpful, and remove ambiguity.
  28. Return ONLY the improved prompt — no explanations.`,
  29. EnableLogging: true,
  30. TimeoutSeconds: 15,
  31. }
  32. // Override with config from Bifrost if provided
  33. if cfgMap, ok := cfg.(map[string]any); ok {
  34. if v, ok := cfgMap["small_model_endpoint"].(string); ok {
  35. config.SmallModelEndpoint = v
  36. }
  37. if v, ok := cfgMap["small_model_name"].(string); ok {
  38. config.SmallModelName = v
  39. }
  40. if v, ok := cfgMap["rewrite_system_prompt"].(string); ok {
  41. config.RewriteSystemPrompt = v
  42. }
  43. if v, ok := cfgMap["enable_logging"].(bool); ok {
  44. config.EnableLogging = v
  45. }
  46. }
  47. return nil
  48. }
  49. func GetName() string {
  50. return "prompt-rewriter"
  51. }
  52. func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
  53. // Only act on chat requests that have messages
  54. if req.ChatRequest == nil || len(req.ChatRequest.Input) == 0 {
  55. return req, nil, nil
  56. }
  57. // Find the last user message
  58. var lastUserMsg *schemas.ChatMessage
  59. var lastUserIdx int
  60. for i := len(req.ChatRequest.Input) - 1; i >= 0; i-- {
  61. if req.ChatRequest.Input[i].Role == "user" {
  62. lastUserMsg = &req.ChatRequest.Input[i]
  63. lastUserIdx = i
  64. break
  65. }
  66. }
  67. if lastUserMsg == nil || lastUserMsg.Content == nil || lastUserMsg.Content.ContentStr == nil {
  68. return req, nil, nil
  69. }
  70. originalPrompt := *lastUserMsg.Content.ContentStr
  71. if strings.TrimSpace(originalPrompt) == "" {
  72. return req, nil, nil
  73. }
  74. if config.EnableLogging {
  75. ctx.Log(schemas.LogLevelInfo, fmt.Sprintf("[prompt-rewriter] Original prompt: %s", truncate(originalPrompt, 120)))
  76. }
  77. // Call the small model to rewrite the prompt
  78. improved, err := rewritePrompt(originalPrompt)
  79. if err != nil {
  80. ctx.Log(schemas.LogLevelWarn, fmt.Sprintf("[prompt-rewriter] Rewrite failed, using original: %v", err))
  81. return req, nil, nil // fail open — keep original prompt
  82. }
  83. if config.EnableLogging {
  84. ctx.Log(schemas.LogLevelInfo, fmt.Sprintf("[prompt-rewriter] Improved prompt: %s", truncate(improved, 120)))
  85. }
  86. // Replace the user message content with the improved version
  87. req.ChatRequest.Input[lastUserIdx].Content = &schemas.ChatMessageContent{
  88. ContentStr: &improved,
  89. }
  90. return req, nil, nil
  91. }
  92. func rewritePrompt(original string) (string, error) {
  93. payload := map[string]any{
  94. "model": config.SmallModelName,
  95. "messages": []map[string]string{
  96. {"role": "system", "content": config.RewriteSystemPrompt},
  97. {"role": "user", "content": original},
  98. },
  99. "temperature": 0.3,
  100. "max_tokens": 1024,
  101. }
  102. body, _ := json.Marshal(payload)
  103. client := &http.Client{Timeout: time.Duration(config.TimeoutSeconds) * time.Second}
  104. resp, err := client.Post(config.SmallModelEndpoint, "application/json", bytes.NewReader(body))
  105. if err != nil {
  106. return "", err
  107. }
  108. defer resp.Body.Close()
  109. if resp.StatusCode != 200 {
  110. b, _ := io.ReadAll(resp.Body)
  111. return "", fmt.Errorf("small model returned %d: %s", resp.StatusCode, string(b))
  112. }
  113. var result struct {
  114. Choices []struct {
  115. Message struct {
  116. Content string `json:"content"`
  117. } `json:"message"`
  118. } `json:"choices"`
  119. }
  120. if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
  121. return "", err
  122. }
  123. if len(result.Choices) == 0 {
  124. return "", fmt.Errorf("no choices returned")
  125. }
  126. return strings.TrimSpace(result.Choices[0].Message.Content), nil
  127. }
  128. func truncate(s string, max int) string {
  129. if len(s) <= max {
  130. return s
  131. }
  132. return s[:max] + "..."
  133. }
  134. // Required stubs
  135. func PreRequestHook(_ *schemas.BifrostContext, _ *schemas.BifrostRequest) error { return nil }
  136. func PostLLMHook(_ *schemas.BifrostContext, resp *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
  137. return resp, err, nil
  138. }
  139. func Cleanup() error { return nil }