commit 1b807b6c9350feab6e61b5e786bd1eba7b939d8e Author: Mason Payne Date: Sun Feb 2 01:02:28 2025 -0700 initial commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/generated-chat-gpt.iml b/.idea/generated-chat-gpt.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/generated-chat-gpt.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..fe20519 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e114035 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module generated-chat-gpt + +go 1.23 diff --git a/main.go b/main.go new file mode 100644 index 0000000..b018995 --- /dev/null +++ b/main.go @@ -0,0 +1,516 @@ +package main + +import ( + "bytes" + "encoding/json" + "html/template" + "io" + "log" + "net/http" + "os" + "time" +) + +// Message represents a single message in the conversation. +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// ChatCompletionRequest is the payload sent to the OpenAI API. +type ChatCompletionRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + // no temperature field for "o3-mini" +} + +// ChatCompletionResponseChoice represents one of the returned choices. +type ChatCompletionResponseChoice struct { + Message Message `json:"message"` +} + +// ChatCompletionResponse is the API response structure. +type ChatCompletionResponse struct { + Choices []ChatCompletionResponseChoice `json:"choices"` + Error *struct { + Message string `json:"message"` + Type string `json:"type"` + } `json:"error,omitempty"` +} + +var ( + openaiAPIKey string + indexTmpl *template.Template +) + +func init() { + openaiAPIKey = os.Getenv("OPENAI_API_KEY") + if openaiAPIKey == "" { + log.Fatal("Environment variable OPENAI_API_KEY is not set") + } + + indexTmpl = template.Must(template.New("index").Parse(indexHTML)) +} + +func main() { + http.HandleFunc("/", indexHandler) + http.HandleFunc("/chat", chatHandler) + http.HandleFunc("/title", titleHandler) // New endpoint for titling conversation + + addr := ":8080" + log.Println("Server starting on", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} + +// indexHandler serves the main HTML page. +func indexHandler(w http.ResponseWriter, r *http.Request) { + if err := indexTmpl.Execute(w, nil); err != nil { + http.Error(w, "Unable to load page", http.StatusInternalServerError) + } +} + +// chatHandler receives conversation messages from the client, +// calls the OpenAI API, and returns the assistant's reply. +func chatHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) + return + } + + var clientRequest struct { + Messages []Message `json:"messages"` + } + if err := json.NewDecoder(r.Body).Decode(&clientRequest); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + apiRequestData := ChatCompletionRequest{ + Model: "o3-mini", + Messages: clientRequest.Messages, + } + + requestBytes, err := json.Marshal(apiRequestData) + if err != nil { + http.Error(w, "Error marshalling request: "+err.Error(), http.StatusInternalServerError) + return + } + + client := http.Client{Timeout: 60 * time.Second} + req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBytes)) + if err != nil { + http.Error(w, "Failed to create request: "+err.Error(), http.StatusInternalServerError) + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+openaiAPIKey) + + apiResponse, err := client.Do(req) + if err != nil { + http.Error(w, "Error calling OpenAI API: "+err.Error(), http.StatusInternalServerError) + return + } + defer apiResponse.Body.Close() + + bodyBytes, err := io.ReadAll(apiResponse.Body) + if err != nil { + http.Error(w, "Error reading API response: "+err.Error(), http.StatusInternalServerError) + return + } + + var completionResponse ChatCompletionResponse + if err := json.Unmarshal(bodyBytes, &completionResponse); err != nil { + http.Error(w, "Error parsing API response: "+err.Error(), http.StatusInternalServerError) + return + } + + if completionResponse.Error != nil { + http.Error(w, "OpenAI API error: "+completionResponse.Error.Message, http.StatusInternalServerError) + return + } + + if len(completionResponse.Choices) == 0 { + http.Error(w, "No choices returned from OpenAI API", http.StatusInternalServerError) + return + } + + responsePayload := map[string]string{ + "reply": completionResponse.Choices[0].Message.Content, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(responsePayload) +} + +// titleHandler provides an automatic title for the conversation. +// It expects a POST with JSON: { messages: []Message } and then +// calls the OpenAI API with a special prompt to generate a title. +func titleHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) + return + } + + // Read the incoming JSON containing the conversation messages. + var clientRequest struct { + Messages []Message `json:"messages"` + } + if err := json.NewDecoder(r.Body).Decode(&clientRequest); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + // Prepend a system message that instructs the AI to generate + // a short title based on the conversation. + titleRequestMessages := []Message{ + { + Role: "system", + Content: "You are an AI that generates conversation titles. Given the conversation below, provide a short, descriptive title (in no more than five words) with no additional commentary.", + }, + } + titleRequestMessages = append(titleRequestMessages, clientRequest.Messages...) + + apiRequestData := ChatCompletionRequest{ + Model: "o3-mini", + Messages: titleRequestMessages, + } + + requestBytes, err := json.Marshal(apiRequestData) + if err != nil { + http.Error(w, "Error marshalling title request: "+err.Error(), http.StatusInternalServerError) + return + } + + client := http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBytes)) + if err != nil { + http.Error(w, "Failed to create title request: "+err.Error(), http.StatusInternalServerError) + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+openaiAPIKey) + + apiResponse, err := client.Do(req) + if err != nil { + http.Error(w, "Error calling OpenAI API for title: "+err.Error(), http.StatusInternalServerError) + return + } + defer apiResponse.Body.Close() + + bodyBytes, err := io.ReadAll(apiResponse.Body) + if err != nil { + http.Error(w, "Error reading title API response: "+err.Error(), http.StatusInternalServerError) + return + } + + var completionResponse ChatCompletionResponse + if err := json.Unmarshal(bodyBytes, &completionResponse); err != nil { + http.Error(w, "Error parsing title API response: "+err.Error(), http.StatusInternalServerError) + return + } + + if completionResponse.Error != nil { + http.Error(w, "OpenAI API title error: "+completionResponse.Error.Message, http.StatusInternalServerError) + return + } + + if len(completionResponse.Choices) == 0 { + http.Error(w, "No title returned from OpenAI API", http.StatusInternalServerError) + return + } + + responsePayload := map[string]string{ + "title": completionResponse.Choices[0].Message.Content, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(responsePayload) +} + +const indexHTML = ` + + + + + ChatGPT Clone - Conversations + + + + + + + + +
+
+ +
+ +
+ +
+
+

Conversation

+
+ +
+
+ + +
+
+
+
+
+
+ + + + + + + +`