this repo has no description
at main 5.2 kB view raw
1package main 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "log/slog" 10 "net/http" 11 12 "github.com/bluesky-social/indigo/pkg/robusthttp" 13) 14 15type LMStudioClient struct { 16 host string 17 httpc *http.Client 18 logger *slog.Logger 19} 20type ResponseSchema struct { 21 Type string `json:"type"` 22 Properties map[string]Property `json:"properties"` 23 Required []string `json:"required"` 24} 25 26type Property struct { 27 Type string `json:"type"` 28 Description string `json:"description,omitempty"` 29 Enum []string `json:"enum,omitempty"` 30} 31 32type ChatRequest struct { 33 Model string `json:"model"` 34 Messages []Message `json:"messages"` 35 Temperature float64 `json:"temperature,omitempty"` 36 MaxTokens int `json:"max_tokens,omitempty"` 37 ResponseFormat *ResponseFormat `json:"response_format,omitempty"` 38} 39 40type Message struct { 41 Role string `json:"role"` 42 Content string `json:"content"` 43} 44 45type ResponseFormat struct { 46 Type string `json:"type"` 47 JSONSchema *JSONSchemaWrap `json:"json_schema,omitempty"` 48} 49 50type JSONSchemaWrap struct { 51 Name string `json:"name"` 52 Schema ResponseSchema `json:"schema"` 53 Strict bool `json:"strict"` 54} 55 56type ChatResponse struct { 57 ID string `json:"id"` 58 Object string `json:"object"` 59 Created int64 `json:"created"` 60 Model string `json:"model"` 61 Choices []Choice `json:"choices"` 62} 63 64type Choice struct { 65 Index int `json:"index"` 66 Message Message `json:"message"` 67 FinishReason string `json:"finish_reason"` 68} 69 70var ( 71 schema = ResponseSchema{ 72 Type: "object", 73 Properties: map[string]Property{ 74 "bad_faith": { 75 Type: "boolean", 76 Description: "Whether the reply to the parent is bad faith or not.", 77 }, 78 "off_topic": { 79 Type: "boolean", 80 Description: "Whether the reply to the parent is off topic.", 81 }, 82 "funny": { 83 Type: "boolean", 84 Description: "Whether the reply to the parent is funny.", 85 }, 86 }, 87 Required: []string{"bad_faith", "off_topic", "funny"}, 88 } 89) 90 91func NewLMStudioClient(host string, logger *slog.Logger) *LMStudioClient { 92 if logger == nil { 93 logger = slog.Default() 94 } 95 logger = logger.With("component", "lmstudio") 96 httpc := robusthttp.NewClient() 97 return &LMStudioClient{ 98 host: host, 99 httpc: httpc, 100 logger: logger, 101 } 102} 103 104func (c *LMStudioClient) sendChatRequest(request ChatRequest) (*ChatResponse, error) { 105 url := fmt.Sprintf("%s/v1/chat/completions", c.host) 106 107 b, err := json.Marshal(request) 108 if err != nil { 109 return nil, fmt.Errorf("error marshaling request: %w", err) 110 } 111 112 resp, err := http.Post(url, "application/json", bytes.NewReader(b)) 113 if err != nil { 114 return nil, fmt.Errorf("error sending request: %w", err) 115 } 116 defer resp.Body.Close() 117 118 body, err := io.ReadAll(resp.Body) 119 if err != nil { 120 return nil, fmt.Errorf("error reading response: %w", err) 121 } 122 123 if resp.StatusCode != http.StatusOK { 124 return nil, fmt.Errorf("bad status code: %d - %s", resp.StatusCode, string(body)) 125 } 126 127 var chatResp ChatResponse 128 if err := json.Unmarshal(body, &chatResp); err != nil { 129 return nil, fmt.Errorf("error unmarshaling response: %w", err) 130 } 131 132 return &chatResp, nil 133} 134 135type BadFaithResults struct { 136 BadFaith bool 137 OffTopic bool 138 Funny bool 139} 140 141func (c *LMStudioClient) GetIsBadFaith(ctx context.Context, parent, reply string) (*BadFaithResults, error) { 142 request := ChatRequest{ 143 Model: "google/gemma-3-27b", 144 Messages: []Message{ 145 { 146 Role: "system", 147 Content: "You are an observer of posts on a microblogging website. You determine if the second message provided by the user is a bad faith reply, an off topic reply, and/or a funny reply to the second message provided to you. Opposing viewpoints are good, and should be appreciated. However, things that are toxic, trollish, or offer no good value to the conversation are considered bad faith. Just because something is bad faith or off topic does not mean the post cannot also be funny.", 148 }, 149 { 150 Role: "user", 151 Content: parent, 152 }, 153 { 154 Role: "user", 155 Content: reply, 156 }, 157 }, 158 Temperature: 0.7, 159 MaxTokens: 100, 160 ResponseFormat: &ResponseFormat{ 161 Type: "json_schema", 162 JSONSchema: &JSONSchemaWrap{ 163 Name: "message_classification", 164 Schema: schema, 165 Strict: true, 166 }, 167 }, 168 } 169 response, err := c.sendChatRequest(request) 170 if err != nil { 171 return nil, fmt.Errorf("failed to get chat response: %w", err) 172 } 173 174 var result map[string]any 175 if err := json.Unmarshal([]byte(response.Choices[0].Message.Content), &result); err != nil { 176 return nil, fmt.Errorf("failed to unmarshal response: %w", err) 177 } 178 179 badFaith, ok := result["bad_faith"].(bool) 180 if !ok { 181 return nil, fmt.Errorf("model gave bad response (bad faith), not structured") 182 } 183 184 offTopic, ok := result["off_topic"].(bool) 185 if !ok { 186 return nil, fmt.Errorf("model gave bad response (off topic), not structured") 187 } 188 189 funny, ok := result["funny"].(bool) 190 if !ok { 191 return nil, fmt.Errorf("model gave bad response (funny), not structured") 192 } 193 194 return &BadFaithResults{ 195 BadFaith: badFaith, 196 OffTopic: offTopic, 197 Funny: funny, 198 }, nil 199}