Skip to content

Commit 005f5f5

Browse files
committed
implement web functionality
1 parent d14fbd7 commit 005f5f5

6 files changed

Lines changed: 238 additions & 79 deletions

File tree

internal/api/server.go

Lines changed: 0 additions & 75 deletions
This file was deleted.

internal/ollama/ollama.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package ollama
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"strings"
9+
10+
"github.com/urfave/cli/v2"
11+
)
12+
13+
func getOllamaRest(url string) ([]byte, error) {
14+
resp, err := http.Get(url)
15+
if err != nil {
16+
return nil, fmt.Errorf("failed to get %s: %w", url, err)
17+
}
18+
defer resp.Body.Close()
19+
20+
body, err := io.ReadAll(resp.Body)
21+
if err != nil {
22+
return nil, fmt.Errorf("failed to read response body: %w", err)
23+
}
24+
25+
return body, nil
26+
}
27+
28+
type ollamaTags struct {
29+
Models []ollamaTag `json:"models"`
30+
}
31+
32+
type ollamaTag struct {
33+
Model string `json:"model"`
34+
Name string `json:"name"`
35+
}
36+
37+
func OllamaConnectionFromContext(c *cli.Context) error {
38+
// get the connection string
39+
connectionString := c.String("ollama-url")
40+
if connectionString == "" {
41+
return fmt.Errorf("ollama-url is required")
42+
}
43+
44+
// make sure the response body is 'Ollama is running'
45+
body, err := getOllamaRest(connectionString)
46+
if err != nil {
47+
return fmt.Errorf("failed to get the response from %s: %w", connectionString, err)
48+
}
49+
if string(body) != "Ollama is running" {
50+
return fmt.Errorf("something was at %s, but it seems like it was not Ollama: %v", connectionString, body)
51+
}
52+
53+
// check that a model of type nomic-embed-text is available
54+
body, err = getOllamaRest(connectionString + "/api/tags")
55+
if err != nil {
56+
return fmt.Errorf("failed to get tags from %s: %w", connectionString, err)
57+
}
58+
59+
var tags ollamaTags
60+
if err := json.Unmarshal(body, &tags); err != nil {
61+
return fmt.Errorf("failed to read the existing models from ollama: %w", err)
62+
}
63+
64+
// for now we hardcode nomic-embed-text
65+
var foundTag ollamaTag
66+
for _, tag := range tags.Models {
67+
if strings.HasPrefix(tag.Model, "nomic-embed-text") {
68+
foundTag = tag
69+
break
70+
}
71+
}
72+
if foundTag == (ollamaTag{}) {
73+
return fmt.Errorf("datailama needs the nomic-embed-text model, which was not found Run \n ollama pull nomic-embed-text:latest\n to get it")
74+
} else {
75+
fmt.Printf("found %v\n", foundTag.Name)
76+
}
77+
return nil
78+
}
Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,68 @@ import (
77

88
"github.com/hydrocode-de/datailama/internal/db"
99
"github.com/hydrocode-de/datailama/internal/sql"
10+
"github.com/hydrocode-de/datailama/internal/version"
1011
)
1112

13+
// VersionOutput defines the response structure for the version endpoint
14+
type VersionOutput struct {
15+
Body struct {
16+
Version string `json:"version" example:"1.0.0" doc:"The version of the application"`
17+
BuildTime string `json:"build_time,omitempty" doc:"The build time of the application"`
18+
GitCommit string `json:"git_commit,omitempty" doc:"The git commit of the application"`
19+
}
20+
}
21+
22+
// TitleSearchOutput defines the response structure for the paper search endpoint
1223
type TitleSearchOutput struct {
1324
Body struct {
1425
Count int `json:"count"`
1526
Paper []sql.SearchPaperByTitleRow `json:"paper"`
1627
}
1728
}
1829

30+
// getVersion handles the version endpoint
31+
func getVersion(ctx context.Context, input *struct{}) (*VersionOutput, error) {
32+
resp := &VersionOutput{}
33+
resp.Body.Version = version.Version
34+
resp.Body.BuildTime = version.BuildTime
35+
resp.Body.GitCommit = version.GitCommit
36+
return resp, nil
37+
}
38+
39+
// searchByTitle handles the paper search endpoint
1940
func searchByTitle(ctx context.Context, input *struct {
2041
Title string `query:"title,omitempty" doc:"The title to search for"`
2142
Author string `query:"author,omitempty" doc:"The author to limit the search to"`
2243
OrderBy string `query:"order,omitempty" example:"citations_year" doc:"The property to order the results by. Can be citations_year or citations"`
2344
Direction string `query:"direction,omitempty" example:"desc" doc:"The direction to order the results by. Can be asc or desc"`
2445
}) (*TitleSearchOutput, error) {
25-
if strings.ToLower(input.OrderBy) != "citations_year" && strings.ToLower(input.OrderBy) != "citations" {
46+
if strings.ToLower(input.OrderBy) != "citations_year" && strings.ToLower(input.OrderBy) != "citations" && input.OrderBy != "" {
2647
return nil, fmt.Errorf("invalid order by argument: %v. Has to be citations_year or citations", input.OrderBy)
2748
}
2849

29-
if strings.ToLower(input.Direction) != "asc" && strings.ToLower(input.Direction) != "desc" {
50+
if strings.ToLower(input.Direction) != "asc" && strings.ToLower(input.Direction) != "desc" && input.Direction != "" {
3051
return nil, fmt.Errorf("invalid direction argument: %v. Has to be asc or desc", input.Direction)
3152
}
3253

54+
// Set defaults
55+
orderBy := input.OrderBy
56+
if orderBy == "" {
57+
orderBy = "citations_year"
58+
}
59+
60+
direction := input.Direction
61+
if direction == "" {
62+
direction = "desc"
63+
}
64+
3365
db := ctx.Value("db").(*db.Manager)
3466

3567
papers, err := db.SearchPaperByTitle(ctx, sql.SearchPaperByTitleParams{
3668
Title: input.Title,
3769
Author: input.Author,
38-
OrderBy: input.OrderBy,
39-
Direction: input.Direction,
70+
OrderBy: orderBy,
71+
Direction: direction,
4072
Limit: 15,
4173
})
4274
if err != nil {

internal/web/api/routes.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package api
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/danielgtaylor/huma/v2"
7+
"github.com/danielgtaylor/huma/v2/adapters/humago"
8+
"github.com/hydrocode-de/datailama/internal/db"
9+
"github.com/hydrocode-de/datailama/internal/version"
10+
)
11+
12+
// RegisterRoutes sets up the API routes using Huma
13+
func RegisterRoutes(router *http.ServeMux, dbManager *db.Manager) {
14+
// Create Huma API on a specific path prefix
15+
api := humago.New(router, huma.DefaultConfig("DataILama API", version.Version))
16+
17+
// Register middleware
18+
api.UseMiddleware(DbMiddleware(dbManager))
19+
20+
// Register API endpoints
21+
huma.Register(api, huma.Operation{
22+
OperationID: "getVersion",
23+
Method: http.MethodGet,
24+
Path: "/api/version",
25+
Summary: "Get the version",
26+
Description: "Get the version of DataILama",
27+
}, getVersion)
28+
29+
huma.Register(api, huma.Operation{
30+
OperationID: "searchPaperByTitle",
31+
Method: http.MethodGet,
32+
Path: "/api/paper/search",
33+
Summary: "Search for Papers by Title",
34+
Description: "Search by Title. Currently the search is a case insensive excact match to a part of the title.",
35+
}, searchByTitle)
36+
}
37+
38+
// DbMiddleware provides database access to API handlers
39+
func DbMiddleware(db *db.Manager) func(ctx huma.Context, next func(huma.Context)) {
40+
return func(ctx huma.Context, next func(huma.Context)) {
41+
newCtx := huma.WithValue(ctx, "db", db)
42+
next(newCtx)
43+
}
44+
}

internal/web/server.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package web
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/hydrocode-de/datailama/internal/db"
7+
"github.com/hydrocode-de/datailama/internal/web/api"
8+
"github.com/hydrocode-de/datailama/internal/web/site"
9+
)
10+
11+
// Server combines both API and site functionality
12+
type Server struct {
13+
DB *db.Manager
14+
Router *http.ServeMux
15+
}
16+
17+
// NewServer creates a new server with both API and site routes
18+
func NewServer(dbManager *db.Manager, apiOnly bool) *Server {
19+
// Create main router
20+
router := http.NewServeMux()
21+
22+
server := &Server{
23+
DB: dbManager,
24+
Router: router,
25+
}
26+
27+
// Register API routes (using Huma)
28+
api.RegisterRoutes(router, dbManager)
29+
30+
// Register frontend routes
31+
if !apiOnly || true {
32+
// also handle the exact path "/" to redirect to /app/ for now
33+
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
34+
http.Redirect(w, r, "/app/", http.StatusSeeOther)
35+
})
36+
//router.Handle("/app/", http.StripPrefix("/app/", http.FileServer(http.Dir("internal/web/site/frontend"))))
37+
router.Handle("/app/", http.StripPrefix("/app/", http.FileServer(http.FS(site.GetEmbedFrontend()))))
38+
}
39+
40+
return server
41+
}
42+
43+
// ServeHTTP implements the http.Handler interface
44+
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
45+
s.Router.ServeHTTP(w, r)
46+
}

internal/web/site/frontend.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package site
2+
3+
import (
4+
"embed"
5+
"io/fs"
6+
"log"
7+
)
8+
9+
// FrontendFS contains the embedded frontend build
10+
//
11+
//go:embed frontend/**/*
12+
//go:embed frontend/*
13+
var FrontendFS embed.FS
14+
15+
func init() {
16+
// Print all embedded files
17+
fs.WalkDir(FrontendFS, ".", func(path string, d fs.DirEntry, err error) error {
18+
if err != nil {
19+
return err
20+
}
21+
if !d.IsDir() {
22+
log.Printf("Embedded file: %s", path)
23+
}
24+
return nil
25+
})
26+
}
27+
28+
func GetEmbedFrontend() fs.FS {
29+
frontendFS, err := fs.Sub(FrontendFS, "frontend")
30+
if err != nil {
31+
panic(err)
32+
}
33+
return frontendFS
34+
}

0 commit comments

Comments
 (0)