From 2c876cef42e8216f612906c289b6d097969bd621 Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Fri, 19 Dec 2025 23:14:28 -0600 Subject: [PATCH] Initial working copy --- .gitignore | 1 + README2.md | 3 + completion/{llama => gollamacpp}/llama.go | 11 +- completion/llama-server/llama-server.go | 116 +++++++++++ completion/llama-server/req.json | 0 completion/llama-server/test.sh | 17 ++ completion/llamacpp/llamacpp.c | 117 +++++++++++ completion/llamacpp/llamacpp.go | 15 ++ completion/llamacpp/llamacpp.h | 1 + completion/openrouter/openrouter.go | 69 +++---- completion/yzma/yzma.go | 2 + db/db.go | 39 ++++ go.mod | 10 + go.sum | 51 ++++- main.go | 86 +++++++-- page/page.go | 74 ------- site/site.go | 225 ++++++++++++++++++++++ templates/404.html | 70 +++++++ templates/index.html | 2 +- 19 files changed, 783 insertions(+), 126 deletions(-) create mode 100644 README2.md rename completion/{llama => gollamacpp}/llama.go (93%) create mode 100644 completion/llama-server/llama-server.go create mode 100644 completion/llama-server/req.json create mode 100644 completion/llama-server/test.sh create mode 100644 completion/llamacpp/llamacpp.c create mode 100644 completion/llamacpp/llamacpp.go create mode 100644 completion/llamacpp/llamacpp.h create mode 100644 db/db.go delete mode 100644 page/page.go create mode 100644 site/site.go diff --git a/.gitignore b/.gitignore index 2960e3b..a07ec20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.gguf svs-services-server +svs-services.db* diff --git a/README2.md b/README2.md new file mode 100644 index 0000000..8fa45e1 --- /dev/null +++ b/README2.md @@ -0,0 +1,3 @@ +Parse this subdomain for a company providing services into English words, if possible. For example, the subdomain "webdesign" should be parsed into the string "Web Design". If the string can't be parsed, just return the original string. Return nothing other than the string, without quotes. The string is "lawnmowing". The parsed string is: + +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 diff --git a/completion/llama/llama.go b/completion/gollamacpp/llama.go similarity index 93% rename from completion/llama/llama.go rename to completion/gollamacpp/llama.go index 4ee51ed..ba7f2a6 100644 --- a/completion/llama/llama.go +++ b/completion/gollamacpp/llama.go @@ -1,4 +1,4 @@ -package llama +package gollamacpp import ( "flag" @@ -9,7 +9,14 @@ import ( "github.com/go-skynet/go-llama.cpp" ) -func main() { +var ( + threads = 4 + tokens = 128 + gpulayers = 0 + seed = -1 +) + +func Run() { var model string flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError) diff --git a/completion/llama-server/llama-server.go b/completion/llama-server/llama-server.go new file mode 100644 index 0000000..e342a9c --- /dev/null +++ b/completion/llama-server/llama-server.go @@ -0,0 +1,116 @@ +package llamaserver + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" +) + +type LlamaServerProvider struct { + Host string // http://localhost:8080/ + Model string +} + +type Message struct { + Role string `json:"role"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Content string `json:"content"` +} + +type Request struct { + Messages []Message `json:"messages"` + Model string `json:"model"` + ChatTemplateKwargs map[string]interface{} `json:"chat_template_kwargs,omitempty"` +} + +type Response struct { + Choices []struct { + Index int `json:"index"` + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Created int64 `json:"created"` // unix timestamp; TODO: decode into time.Time + Model string `json:"model"` + SystemFingerprint string `json:"system_fingerprint"` + Object string `json:"object"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` + ID string `json:"id"` + Timings struct { + CacheN int `json:"cache_n"` + PromptN int `json:"prompt_n"` + PromptMS float64 `json:"prompt_ms"` + PromptPerTokenMS float64 `json:"prompt_per_token_ms"` + PromptPerSecond float64 `json:"prompt_per_second"` + PredictedN int `json:"predicted_n"` + PredictedMS float64 `json:"predicted_ms"` + PredictedPerTokenMS float64 `json:"predicted_per_token_ms"` + PredictedPerSecond float64 `json:"predicted_per_second"` + } `json:"timings"` +} + +func (p LlamaServerProvider) Health() (err error) { + client := http.Client{ + Timeout: 100 * time.Millisecond, + } + res, err := client.Get(p.Host + "health") + if err != nil { + return err + } + if res.StatusCode != 200 { + return fmt.Errorf("llama-server health check returned status %v (%v)", res.StatusCode, res.Status) + } + return nil +} + +func (p LlamaServerProvider) Complete(ctx context.Context, prompt string) (response string, err error) { + req := Request{ + Messages: []Message{ + { + Role: "user", + Content: prompt, + }, + }, + Model: p.Model, + ChatTemplateKwargs: map[string]interface{}{ + "reasoning_effort": "low", + }, + } + encReq, err := json.Marshal(req) + if err != nil { + return "", fmt.Errorf("marshaling json: %w", err) + } + res, err := http.Post(p.Host+"/v1/chat/completions", "application/json", bytes.NewReader(encReq)) + if err != nil { + return "", err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return "", fmt.Errorf("reading response body: %w", err) + } + log.Println(string(body)) + + resData := Response{} + dec := json.NewDecoder(bytes.NewReader(body)) + if err := dec.Decode(&resData); err != nil { + return "", fmt.Errorf("decoding response: %w", err) + } + if len(resData.Choices) == 0 { + log.Println(resData) + return "", fmt.Errorf("no choices in response") + } + + log.Printf("Generated %v (%v) tokens in %v ms (%v T/s)", resData.Usage.CompletionTokens, resData.Timings.PredictedN, resData.Timings.PredictedMS, resData.Timings.PredictedPerSecond) + + return resData.Choices[0].Message.Content, nil +} diff --git a/completion/llama-server/req.json b/completion/llama-server/req.json new file mode 100644 index 0000000..e69de29 diff --git a/completion/llama-server/test.sh b/completion/llama-server/test.sh new file mode 100644 index 0000000..8665d16 --- /dev/null +++ b/completion/llama-server/test.sh @@ -0,0 +1,17 @@ +curl 'http://localhost:8080/v1/chat/completions' \ + -X POST \ + -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0' \ + -H 'Accept: */*' \ + -H 'Accept-Language: en-US,en;q=0.5' \ + -H 'Accept-Encoding: gzip, deflate, br, zstd' \ + -H 'Referer: http://localhost:8080/' \ + -H 'Content-Type: application/json' \ + -H 'Origin: http://localhost:8080' \ + -H 'Connection: keep-alive' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: same-origin' \ + -H 'Priority: u=4' \ + -H 'Pragma: no-cache' \ + -H 'Cache-Control: no-cache' \ + --data @req.json diff --git a/completion/llamacpp/llamacpp.c b/completion/llamacpp/llamacpp.c new file mode 100644 index 0000000..7cf2457 --- /dev/null +++ b/completion/llamacpp/llamacpp.c @@ -0,0 +1,117 @@ +#include "llama.h" +#include +#include +#include +#include + +void null_log_callback(enum ggml_log_level level, const char *message, void *user_data) {} + +int64_t time_us(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec*1000000 + (int64_t)ts.tv_nsec/1000; +} + +void chandlerscustomllama(char* prompt) { + int n_predict = 100; + + printf("Prompt: %s\n", prompt); + struct llama_model_params model_params = llama_model_default_params(); + // printf("model_params.n_gpu_layers: %d\n", model_params.n_gpu_layers); + llama_log_set(null_log_callback, NULL); // Disable logging + + struct llama_model *model = llama_model_load_from_file("/home/chandler/llms/gpt-oss-20b-Q4_K_M.gguf", model_params); + if (model == NULL) { + fprintf(stderr, "Failed to load model\n"); + return; + } + + const struct llama_vocab * vocab = llama_model_get_vocab(model); + + const int n_prompt = -llama_tokenize(vocab, prompt, strlen(prompt), NULL, 0, true, true); + + llama_token * prompt_tokens = malloc(sizeof(llama_token) * n_prompt); + if (llama_tokenize(vocab, prompt, strlen(prompt), prompt_tokens, n_prompt, true, true) < 0) { + fprintf(stderr, "%s: error: failed to tokenize prompt\n", __func__); + return; + } + + struct llama_context_params ctx_params = llama_context_default_params(); + ctx_params.n_ctx = n_prompt + n_predict - 1; + ctx_params.n_batch = n_prompt; + ctx_params.no_perf = false; // TODO: true + + struct llama_context * ctx = llama_init_from_model(model, ctx_params); + if (ctx == NULL) { + fprintf(stderr, "%s: error: failed to create llama_context\n", __func__); + return; + } + + // initialize the sampler + struct llama_sampler_chain_params sparams = llama_sampler_chain_default_params(); + struct llama_sampler * smpl = llama_sampler_chain_init(sparams); + llama_sampler_chain_add(smpl, llama_sampler_init_greedy()); + + // prepare a batch for the prompt + struct llama_batch batch = llama_batch_get_one(prompt_tokens, n_prompt); + + if (llama_model_has_encoder(model)) { + if (llama_encode(ctx, batch)) { + fprintf(stderr, "%s : failed to eval\n", __func__); + } + + llama_token decoder_start_token_id = llama_model_decoder_start_token(model); + if (decoder_start_token_id == LLAMA_TOKEN_NULL) { + decoder_start_token_id = llama_vocab_bos(vocab); + } + + batch = llama_batch_get_one(&decoder_start_token_id, 1); + } + + int64_t start = time_us(); + int n_decode = 0; + llama_token new_token_id; + + for (int n_pos = 0; n_pos + batch.n_tokens < n_prompt + n_predict; ) { + // evaluate the current batch with the transformer model + if (llama_decode(ctx, batch)) { + fprintf(stderr, "%s : failed to eval\n", __func__); + } + + n_pos += batch.n_tokens; + + + // sample the next token + { + new_token_id = llama_sampler_sample(smpl, ctx, -1); + + // is it an end of generation? + if (llama_vocab_is_eog(vocab, new_token_id)) { + break; + } + + char buf[128]; // TODO: how do we know that this is enough? + int n = llama_token_to_piece(vocab, new_token_id, buf, sizeof(buf), 0, true); // TODO: do I want special tokens? + if (n < 0) { + fprintf(stderr, "%s: error: failed to convert token to piece\n", __func__); + return; + } + buf[n] = 0; + printf("%s", buf); // TODO: null terminator? + + // prepare the next batch with the sampled token + batch = llama_batch_get_one(&new_token_id, 1); + + n_decode += 1; + } + } + + + int64_t end = time_us(); + + fprintf(stderr, "%s: decoded %d tokens in %.2f s, speed: %.2f t/s\n", + __func__, n_decode, (end - start) / 1000000.0f, n_decode / ((end - start) / 1000000.0f)); + llama_sampler_free(smpl); + llama_free(ctx); + llama_model_free(model); +} diff --git a/completion/llamacpp/llamacpp.go b/completion/llamacpp/llamacpp.go new file mode 100644 index 0000000..4bb579e --- /dev/null +++ b/completion/llamacpp/llamacpp.go @@ -0,0 +1,15 @@ +package llamacpp + +/* +#include "llamacpp.h" +#include +*/ +import "C" + +import "unsafe" + +func Run() { + prompt := C.CString("Here is a very short story about a brave knight. One day, the knight") + C.chandlerscustomllama(prompt) + C.free(unsafe.Pointer(prompt)) +} diff --git a/completion/llamacpp/llamacpp.h b/completion/llamacpp/llamacpp.h new file mode 100644 index 0000000..2767cfa --- /dev/null +++ b/completion/llamacpp/llamacpp.h @@ -0,0 +1 @@ +void chandlerscustomllama(char* prompt); diff --git a/completion/openrouter/openrouter.go b/completion/openrouter/openrouter.go index 9f0df96..f070208 100644 --- a/completion/openrouter/openrouter.go +++ b/completion/openrouter/openrouter.go @@ -5,10 +5,17 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" "time" ) +type OpenRouterProvider struct { + // Endpoint string + Token string + Model string // "openai/gpt-oss-20b:free" +} + type Message struct { Role string `json:"role"` // "system" | "user" | "assistant" Content string `json:"content"` @@ -21,6 +28,10 @@ type ChatCompletionRequest struct { MaxTokens *int `json:"max_tokens,omitempty"` TopP *float64 `json:"top_p,omitempty"` Stop json.RawMessage `json:"stop,omitempty"` // string or []string; keep flexible + Provider struct { + Sort string `json:"sort,omitempty"` + } `json:"provider,omitempty"` + } type ChatCompletionResponse struct { @@ -35,60 +46,52 @@ type ChatCompletionResponse struct { } `json:"choices"` } -func ChatCompletion(ctx context.Context, req ChatCompletionRequest) (ChatCompletionResponse, error) { - httpClient := http.Client{Timeout: 10 * time.Second} +func (p OpenRouterProvider) Complete(ctx context.Context, prompt string) (string, error) { + req := ChatCompletionRequest{ + Model: p.Model, + Messages: []Message{ + { + Role: "user", + Content: prompt, + }, + }, + Provider: struct { + Sort string `json:"sort,omitempty"` + }{ + Sort: "throughput", + }, + } + httpClient := http.Client{Timeout: 10 * time.Second} body, err := json.Marshal(req) if err != nil { - return ChatCompletionResponse{}, err + return "", err } httpReq, err := http.NewRequestWithContext(ctx, "POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewReader(body)) if err != nil { - return ChatCompletionResponse{}, err + return "", err } - httpReq.Header.Set("Authorization", "Bearer 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") + httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %v", p.Token)) httpReq.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(httpReq) if err != nil { - return ChatCompletionResponse{}, err + return "", err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { // You may want to decode OpenRouter's error JSON here for better messages - return ChatCompletionResponse{}, fmt.Errorf("openrouter status %d", resp.StatusCode) + return "", fmt.Errorf("openrouter status %d", resp.StatusCode) } var out ChatCompletionResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return ChatCompletionResponse{}, err - } - return out, nil -} - -func doLLM() { - - req := ChatCompletionRequest{ - Model: "openai/gpt-oss-20b:free", - Messages: []Message{ - { - Role: "user", - Content: "Write a short poem about software development.", - }, - }, - } - - ctx := context.Background() - resp, err := client.ChatCompletion(ctx, req) - if err != nil { - fmt.Println("Error:", err) - return - } - - for _, choice := range resp.Choices { - fmt.Printf("Response: %s\n", choice.Message.Content) + log.Println(err) + log.Println(out) + return "", err } + return out.Choices[0].Message.Content, nil } diff --git a/completion/yzma/yzma.go b/completion/yzma/yzma.go index 1329737..f50bfbf 100644 --- a/completion/yzma/yzma.go +++ b/completion/yzma/yzma.go @@ -49,4 +49,6 @@ func Complete(ctx context.Context, prompt string) (string, error) { } fmt.Println() + + return "", nil } diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..3df2c26 --- /dev/null +++ b/db/db.go @@ -0,0 +1,39 @@ +package db + +import ( + "database/sql" + "log" + + _ "modernc.org/sqlite" +) + +var DB *sql.DB + +func InitDatabase(dbPath string) error { + var err error + DB, err = sql.Open("sqlite", dbPath) + if err != nil { + return err + } + + _, err = DB.Exec(` + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + `) + if err != nil { + log.Fatal(err) + } + + _, err = DB.Exec( + `CREATE TABLE IF NOT EXISTS sites ( + subdomain TEXT PRIMARY KEY, + data BLOB NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + view_count INTEGER DEFAULT 1 + )`, + ) + if err != nil { + return err + } + return nil +} diff --git a/go.mod b/go.mod index 38c9877..dccd5dc 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,20 @@ go 1.25.4 require ( github.com/go-skynet/go-llama.cpp v0.0.0-20240314183750-6a8041ef6b46 github.com/hybridgroup/yzma v1.3.0 + modernc.org/sqlite v1.41.0 ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jupiterrider/ffi v0.5.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect golang.org/x/sys v0.38.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index e967abd..f26041f 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= @@ -8,23 +10,64 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hybridgroup/yzma v1.3.0 h1:5dw9qEcFEGEJq+tA12Ooa6D/e0PROqv7Ix6VfSR9MQI= github.com/hybridgroup/yzma v1.3.0/go.mod h1:UUYw+DLlrgtBYm+B+9XD3boB1ZcDpfbAnYHKW3VKKZ4= github.com/jupiterrider/ffi v0.5.1 h1:l7ANXU+Ex33LilVa283HNaf/sTzCrrht7D05k6T6nlc= github.com/jupiterrider/ffi v0.5.1/go.mod h1:x7xdNKo8h0AmLuXfswDUBxUsd2OqUP4ekC8sCnsmbvo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c= github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck= +modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/main.go b/main.go index 1da7225..fd5580b 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,19 @@ package main import ( "embed" + "flag" + "fmt" "html/template" + "log" "net/http" + "os" + "strings" - "git.chandlerswift.com/chandlerswift/svs-services-server/page" + "git.chandlerswift.com/chandlerswift/svs-services-server/completion" + llamaserver "git.chandlerswift.com/chandlerswift/svs-services-server/completion/llama-server" + "git.chandlerswift.com/chandlerswift/svs-services-server/completion/openrouter" + "git.chandlerswift.com/chandlerswift/svs-services-server/db" + "git.chandlerswift.com/chandlerswift/svs-services-server/site" ) //go:embed templates/index.html @@ -20,20 +29,73 @@ var headshots embed.FS var tmpl = template.Must(template.New("index").Parse(indexTemplate)) var notFoundTmpl = template.Must(template.New("404").Parse(notFoundTemplate)) +var completionProviders map[string]completion.CompletionProvider = map[string]completion.CompletionProvider{ + "openrouter": openrouter.OpenRouterProvider{ + Token: os.Getenv("OPENROUTER_API_KEY"), + Model: "openai/gpt-oss-120b", + }, + "llama-server": llamaserver.LlamaServerProvider{ + Host: "http://localhost:8080", + Model: "gpt-oss-20b-Q4_K_M", + }, +} + +func parseService(host string) (h string, err error) { + parts := strings.Split(host, ".") + if len(parts) != 3 { + return "", fmt.Errorf("Unexpected number of parts in hostname; expected 3, got %v", len(parts)) + } + return parts[0], nil +} + func main() { - http.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { - host := r.Host - pageData := page.GenerateDummyData(host) - tmpl.Execute(w, pageData) + // use flag package to parse args: + port := flag.String("port", "64434", "Port to listen on") // echo "svs" | base64 -d | od -An -t u2 + address := flag.String("address", "", "Address to listen on") + completionProviderName := flag.String("completion-provider", "llama-server", "Completion provider to use (openrouter, llama-server, …)") + dbpath := flag.String("database", "svs-services.db", "Path to SQLite database file") + flag.Parse() + + err := db.InitDatabase(*dbpath) + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + + addr := fmt.Sprintf("%s:%s", *address, *port) + + fmt.Println("Using completion provider", *completionProviderName) + defaultCompletionProvider := completionProviders[*completionProviderName] + // for err := defaultCompletionProvider.Health(); err != nil; err = defaultCompletionProvider.Health() { + // fmt.Println("Waiting for Llama server to be healthy... (%v)", err) + // time.Sleep(1 * time.Second) + // } + + http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + service, err := parseService(r.Host) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + + siteData, err := site.GetSiteData(service, defaultCompletionProvider) + if err != nil { + http.Error(w, "Could not generate site", 500) + log.Printf("Getting site data for service %v: %v", service, err) + return + } + + if r.URL.Path != "/" { + w.WriteHeader(http.StatusNotFound) + notFoundTmpl.Execute(w, siteData) + return + } + + tmpl.Execute(w, siteData) }) // Serve embedded headshots file - http.Handle("/headshots/", http.FileServer(http.FS(headshots))) + http.Handle("GET /headshots/", http.FileServer(http.FS(headshots))) - // 404 handler - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - notFoundTmpl.Execute(w, nil) - }) + fmt.Println("Starting server on", addr) + panic(http.ListenAndServe(addr, nil)) - http.ListenAndServe(":8080", nil) // TODO: configurable port } diff --git a/page/page.go b/page/page.go deleted file mode 100644 index b7149ba..0000000 --- a/page/page.go +++ /dev/null @@ -1,74 +0,0 @@ -package page - -import "time" - -type Color struct { // may have .rgba() and .hex() and similar - Hex string - // something that can encode color -} - -type Theme struct { - AccentColor Color - SecondaryColor Color - BackgroundColor Color - // possibly more? -} - -type Testimonial struct { - Name string - Position string - Quote string -} - -type Page struct { - Theme Theme - Occupation string - Tagline string - WhatWeDo string - ChandlerBio string - EricBio string - IsaacBio string - Testimonials []Testimonial - CurrentYear int -} - -func GenerateDummyData(host string) Page { - return Page{ - Theme: Theme{ - AccentColor: Color{ - Hex: "#FF5733", - }, - SecondaryColor: Color{ - Hex: "#33C1FF", - }, - BackgroundColor: Color{ - // Creamy off-white - Hex: "#FFF8E7", - }, - }, - Occupation: "Web Design", - Tagline: "Custom websites cooked to order", - WhatWeDo: "Lorem Ipsum…", - ChandlerBio: "Chandler has years of experience in the [...] sector", - EricBio: "Eric is a seasoned professional in [...]", - IsaacBio: "Isaac specializes in [...]", - Testimonials: []Testimonial{ - { - Name: "Barack Obama", - Position: "Poolhall acquaintance", - Quote: "After the fiasco of the ACA Website, I know how important it is to get a website right on the first try, and boy do these gentlemen deliver!", - }, - { - Name: "Ada Lovelace", - Position: "Pioneer of Computing", - Quote: "The elegance and functionality of the websites created by SVS Web Design are truly ahead of their time.", - }, - { - Name: "Steve Jobs", - Position: "Visionary Entrepreneur", - Quote: "Design is not just what it looks like and feels like. Design is how it works. SVS Web Design understands this principle deeply.", - }, - }, - CurrentYear: time.Now().Year(), - } -} diff --git a/site/site.go b/site/site.go new file mode 100644 index 0000000..37a6a95 --- /dev/null +++ b/site/site.go @@ -0,0 +1,225 @@ +package site + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "html/template" + "regexp" + "strings" + "time" + + "git.chandlerswift.com/chandlerswift/svs-services-server/completion" + "git.chandlerswift.com/chandlerswift/svs-services-server/db" +) + +type Color struct { // may have .rgba() and .hex() and similar + Hex string + // something that can encode color +} + +type Theme struct { + AccentColor Color + SecondaryColor Color + BackgroundColor Color + // possibly more? +} + +type Testimonial struct { + Name string + Position string + Quote string +} + +type Site struct { + Theme Theme + Occupation string + Tagline string + WhatWeDo template.HTML + ChandlerBio template.HTML + EricBio template.HTML + IsaacBio template.HTML + Testimonials []Testimonial + CurrentYear int +} + +var whitespace = regexp.MustCompile(`\s+`) + +func norm(s string) string { + return strings.TrimSpace(whitespace.ReplaceAllString(s, " ")) +} + +func GetSiteData(subdomain string, cp completion.CompletionProvider) (site *Site, err error) { + var raw []byte + err = db.DB.QueryRow(`UPDATE sites SET view_count = view_count + 1 WHERE subdomain = ? RETURNING data`, subdomain).Scan(&raw) + if err == nil { + site = &Site{} + err = json.Unmarshal(raw, site) + if err != nil { + return nil, fmt.Errorf("unmarshaling site data: %w", err) + } + return site, nil + } else if err == sql.ErrNoRows { + site, err = GenerateSiteData(subdomain, cp) + if err != nil { + return nil, fmt.Errorf("generating site: %w", err) + } + data, err := json.Marshal(site) + if err != nil { + return nil, fmt.Errorf("marshaling generated site: %w", err) + } + _, err = db.DB.Exec(`INSERT INTO sites (subdomain, data) VALUES (?, ?)`, subdomain, data) + if err != nil { + return nil, fmt.Errorf("inserting generated site: %w", err) + } + return site, nil + } else { + return nil, fmt.Errorf("querying site data: %w", err) + } +} + +func GenerateSiteData(rawservice string, cp completion.CompletionProvider) (site *Site, err error) { + site = &Site{} + + prompt := norm(` + Parse this subdomain for a company providing services into English words, if possible. + For example, the subdomain "webdesign" should be parsed into the string "Web Design". + If the string can't be parsed, just return the original string. + Return nothing other than the string, without quotes. + The string is "%s". + The parsed string is: + `) + prompt = strings.TrimSpace(prompt) + prompt = whitespace.ReplaceAllString(prompt, " ") + + site.Occupation, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, rawservice)) + if err != nil { + return nil, fmt.Errorf("parsing service: %w", err) + } + + prompt = norm(` + Generate a short, catchy tagline for a %v company. + The tagline should be no more than 7 words long, and should be slightly silly; + e.g. for a web design company, something like "Custom websites cooked to order". + Do not include quotes. + `) + site.Tagline, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + if err != nil { + return nil, fmt.Errorf("generating tagline: %w", err) + } + + site.Theme = Theme{ // TODO + AccentColor: Color{ + Hex: "#FF5733", + }, + SecondaryColor: Color{ + Hex: "#33C1FF", + }, + BackgroundColor: Color{ + // Creamy off-white + Hex: "#FFF8E7", + }, + } + + prompt = norm(` + Generate a few short paragraphs (3-4 sentences each) describing what the offices of Swift, Villnow & Swift + (colloquially \"SVS\"), a %v company, has to offer, in an over-the-top, slightly irreverent tone. + The paragraphs will go on the company's homesite to inform visitors about their services. + Markup should be in HTML, including paragraph breaks. + Do not wrap in any additional markup. + `) + whatWeDo, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + site.WhatWeDo = template.HTML(whatWeDo) + if err != nil { + return nil, fmt.Errorf("generating what we do: %w", err) + } + + prompt = norm(` + Generate a short bio (2-4 sentences) for Chandler Swift, a co-founder of SVS, a %v company. + Chandler went to high school in Glencoe, Minnesota with Eric Villnow and Isaac Swift, the other two co-founders. + Chandler has always enjoyed tinkering with projects. + Chandler has a degree in Computer Science from the University of Minnesota, Duluth. + Chandler spent years with the Boy Scouts (including a summer working at Many Point Scout Camp) and is an Eagle Scout. + Chandler plays keyboard instruments, especially the piano (jazz and classical), and the organ. + Chandler enjoys long-distance bicycling. + Chandler has a cocker spaniel-poodle mix Mabel. + Feel free to make up relevant work history, as long as it's over the top; + for example, for SVS Space Sciences division, you might say that he spent 6 months on the International Space Station and/or was the Administrator of NASA. + Feel free to make up awards, again humorous and over the top, e.g. "In 2019 he swept the Nobel ceremony, taking home prizes in Chemistry, Physics _and_ Peace (the Peace Prize was awarded for _not_ releasing the technology behind the Physics Prize!). + Emphasize the relevance of all the above information to SVS's line of work. + Do not shoehorn all the above information; only include each piece if relevant. + `) + bio, err := cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + if err != nil { + return nil, fmt.Errorf("generating Chandler bio: %w", err) + } + site.ChandlerBio = template.HTML(bio) + + prompt = norm(` + Generate a short, satirical/irreverent bio (2-4 sentences) for Eric Villnow, a co-founder of SVS, a %v company. + Eric went to high school in Glencoe, Minnesota with Isaac Swift and Chandler Swift, the other two co-founders. + Eric has a degree in Computer Science from Bemidji State University with a minor in Business Administration. + Eric drives a 2023 Toyota Tacoma TRD Off-Road. + Eric works as a UPS driver west of the Twin Cities, and has a spotless safety record. + Eric drinks vast quantities of Mountain Dew and 1919 Root Beer. + Eric's hobbies include farming, where he works on his family's farm near Plato, Minnesota. + Eric enjoys woodworking and 3D printing. + Feel free to make up relevant work history, as long as it's over the top; + for example, saying that he delivered two hundred boxes in his first day as a UPS driver -- after his truck broke down five miles into the route! + Also, make up awards, again humorous and over the top, e.g. "In 2023 he was naed the fastest UPS driver, with a record showing he averaged package delivery 15 minutes before they were even shipped." + Emphasize the relevance of all the above information to SVS's line of work. + Do not include all the above information; only mention each piece if relevant. + `) + bio, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + if err != nil { + return nil, fmt.Errorf("generating Isaac bio: %w", err) + } + site.EricBio = template.HTML(fmt.Sprintf("Eric is a seasoned professional in the %v industry, known for his expertise and dedication.", site.Occupation)) + + prompt = norm(` + Generate a short, satirical/irreverent bio (2-4 sentences) for Isaac Swift, a co-founder of SVS, a %v company. + Isaac went to high school in Glencoe, Minnesota with Eric Villnow and Chandler Swift, the other two co-founders. + Isaac studied Computer Science for a year at North Dakota State University. + Isaac enjoys mountain biking and camping. + Isaac drives a 2024 Toyota Tacoma TRD Off-Road, and won't shut up about it. + Isaac is an avid adventure motorcyclist. + Isaac worked eight summers at Many Point Scout Camp, occupying positions from program counselor to camp director. + While there, he designed and ran a new mountain biking program. + Isaac works Emergency Medical Services as an EMT for Allina Ambulance Service in Hutchinson, Minnesota. + If relevant, mention that "Isaac is not only cute enough to stop your heart, but skilled enough to restart it!". + Heavily emphasize Isaac's coffee consumption, and possibly mention his role as lead coffee engineer at SVS. + Feel free to make up relevant work history, as long as it's over the top; + for example, saying that he saved several thousand lives his first week as an EMT. + Also, make up awards, again humorous and over the top, e.g. "In 2022 he was named EMT of the Year for resuscitating a patient who had been dead for nearly six months." + Emphasize the relevance of all the above information to SVS's line of work. + Do not include all the above information; only mention each piece if relevant. + `) + bio, err = cp.Complete(context.TODO(), fmt.Sprintf(prompt, site.Occupation)) + if err != nil { + return nil, fmt.Errorf("generating Isaac bio: %w", err) + } + site.IsaacBio = template.HTML(bio) + + + + site.Testimonials = []Testimonial{ + { + Name: "Barack Obama", + Position: "Poolhall acquaintance", + Quote: "After the fiasco of the ACA Website, I know how important it is to get a website right on the first try, and boy do these gentlemen deliver!", + }, + { + Name: "Ada Lovelace", + Position: "Pioneer of Computing", + Quote: "The elegance and functionality of the websites created by SVS Web Design are truly ahead of their time.", + }, + { + Name: "Steve Jobs", + Position: "Visionary Entrepreneur", + Quote: "Design is not just what it looks like and feels like. Design is how it works. SVS Web Design understands this principle deeply.", + }, + } + site.CurrentYear = time.Now().Year() + return +} diff --git a/templates/404.html b/templates/404.html index e69de29..9940f6b 100644 --- a/templates/404.html +++ b/templates/404.html @@ -0,0 +1,70 @@ + + + + + + 404 — Page not found + + + + + + diff --git a/templates/index.html b/templates/index.html index ca1fe7c..d7cb0a4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@ - Swift, Villnow & Swift Industries: {{.Occupation}} + SVS {{.Occupation}}