diff --git a/pkg/app/api_client.go b/pkg/app/api_client.go index de9ebb2..929dbbc 100644 --- a/pkg/app/api_client.go +++ b/pkg/app/api_client.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "net/url" "sync" "github.com/julwrites/ScriptureBot/pkg/secrets" @@ -87,6 +88,16 @@ var SubmitQuery = func(req QueryRequest, result interface{}) error { {Verse: "John 3:16", URL: "https://example.com/John3:16"}, }, } + case *PromptResponse: + *r = PromptResponse{ + Data: OQueryResponse{ + Text: "This is a mock response.", + References: []SearchResult{ + {Verse: "John 3:16", URL: "https://example.com/John3:16"}, + }, + }, + Meta: Meta{AIProvider: "mock"}, + } case *VerseResponse: *r = VerseResponse{ Verse: "For God so loved the world...", @@ -137,3 +148,84 @@ var SubmitQuery = func(req QueryRequest, result interface{}) error { return nil } + +// GetVersions retrieves the list of available Bible versions. +var GetVersions = func(page, limit int, name, language, sort string) (*VersionsResponse, error) { + apiURL, apiKey := getAPIConfig() + if apiURL == "" { + return nil, fmt.Errorf("BIBLE_API_URL environment variable is not set") + } + + // Mock response for tests + if apiURL == "https://example.com" { + return &VersionsResponse{ + Data: []Version{ + {Code: "ESV", Name: "English Standard Version", Language: "English"}, + {Code: "NIV", Name: "New International Version", Language: "English"}, + }, + Total: 2, + Page: 1, + Limit: 20, + }, nil + } + + client := &http.Client{} + + reqURL, err := url.Parse(apiURL + "/bible-versions") + if err != nil { + return nil, fmt.Errorf("failed to parse url: %v", err) + } + q := reqURL.Query() + if page > 0 { + q.Set("page", fmt.Sprintf("%d", page)) + } + if limit > 0 { + q.Set("limit", fmt.Sprintf("%d", limit)) + } + if name != "" { + q.Set("name", name) + } + if language != "" { + q.Set("language", language) + } + if sort != "" { + q.Set("sort", sort) + } + reqURL.RawQuery = q.Encode() + + httpReq, err := http.NewRequest("GET", reqURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + if apiKey != "" { + httpReq.Header.Set("X-API-KEY", apiKey) + } + + resp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("request failed: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + if resp.StatusCode != http.StatusOK { + var errResp ErrorResponse + if jsonErr := json.Unmarshal(body, &errResp); jsonErr == nil && errResp.Error.Message != "" { + return nil, fmt.Errorf("api error (%d): %s", errResp.Error.Code, errResp.Error.Message) + } + return nil, fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(body)) + } + + var result VersionsResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %v", err) + } + + return &result, nil +} diff --git a/pkg/app/api_client_test.go b/pkg/app/api_client_test.go index 88fb4ef..8ebdbbf 100644 --- a/pkg/app/api_client_test.go +++ b/pkg/app/api_client_test.go @@ -28,7 +28,7 @@ func TestSubmitQuery(t *testing.T) { // Avoid using Prompt ("hello") as it triggers the LLM which might be unstable (500 errors). req := QueryRequest{ Query: QueryObject{Verses: []string{"John 3:16"}}, - Context: QueryContext{User: UserContext{Version: "NIV"}}, + User: UserOptions{Version: "NIV"}, } var resp VerseResponse err := SubmitQuery(req, &resp) @@ -53,3 +53,32 @@ func TestSubmitQuery(t *testing.T) { } }) } + +func TestGetVersions(t *testing.T) { + t.Run("Success", func(t *testing.T) { + defer SetEnv("BIBLE_API_URL", "https://example.com")() + defer SetEnv("BIBLE_API_KEY", "api_key")() + ResetAPIConfigCache() + + resp, err := GetVersions(1, 10, "", "", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if resp.Total != 2 { + t.Errorf("Expected total 2, got %d", resp.Total) + } + if len(resp.Data) != 2 { + t.Errorf("Expected 2 versions, got %d", len(resp.Data)) + } + }) + + t.Run("No URL", func(t *testing.T) { + defer SetEnv("BIBLE_API_URL", "")() + ResetAPIConfigCache() + + _, err := GetVersions(1, 10, "", "", "") + if err == nil { + t.Error("Expected error when BIBLE_API_URL is unset") + } + }) +} diff --git a/pkg/app/api_models.go b/pkg/app/api_models.go index c439992..0f550b4 100644 --- a/pkg/app/api_models.go +++ b/pkg/app/api_models.go @@ -2,27 +2,33 @@ package app // Request Models -type UserContext struct { - Version string `json:"version,omitempty"` +type UserOptions struct { + Version string `json:"version,omitempty"` + AIProvider string `json:"ai_provider,omitempty"` +} + +type Options struct { + Stream bool `json:"stream,omitempty"` } type QueryContext struct { - History []string `json:"history,omitempty"` - Schema string `json:"schema,omitempty"` - Verses []string `json:"verses,omitempty"` - Words []string `json:"words,omitempty"` - User UserContext `json:"user,omitempty"` + History []string `json:"history,omitempty"` + Schema string `json:"schema,omitempty"` + Verses []string `json:"verses,omitempty"` + Words []string `json:"words,omitempty"` } type QueryObject struct { - Verses []string `json:"verses,omitempty"` - Words []string `json:"words,omitempty"` - Prompt string `json:"prompt,omitempty"` + Verses []string `json:"verses,omitempty"` + Words []string `json:"words,omitempty"` + Prompt string `json:"prompt,omitempty"` + Context QueryContext `json:"context,omitempty"` } type QueryRequest struct { - Query QueryObject `json:"query"` - Context QueryContext `json:"context,omitempty"` + Query QueryObject `json:"query"` + User UserOptions `json:"user,omitempty"` + Options Options `json:"options,omitempty"` } // Response Models @@ -44,6 +50,15 @@ type OQueryResponse struct { References []SearchResult `json:"references"` } +type Meta struct { + AIProvider string `json:"ai_provider"` +} + +type PromptResponse struct { + Data OQueryResponse `json:"data"` + Meta Meta `json:"meta"` +} + type ErrorResponse struct { Error ErrorDetails `json:"error"` } @@ -52,3 +67,17 @@ type ErrorDetails struct { Code int `json:"code"` Message string `json:"message"` } + +type Version struct { + Name string `json:"name"` + Code string `json:"code"` + Language string `json:"language"` + Providers map[string]string `json:"providers"` +} + +type VersionsResponse struct { + Data []Version `json:"data"` + Total int `json:"total"` + Page int `json:"page"` + Limit int `json:"limit"` +} diff --git a/pkg/app/api_models_test.go b/pkg/app/api_models_test.go index 8d5378f..7d1dc2f 100644 --- a/pkg/app/api_models_test.go +++ b/pkg/app/api_models_test.go @@ -12,16 +12,16 @@ func TestQueryRequest_Marshal(t *testing.T) { Verses: []string{"John 3:16"}, Words: []string{"Love"}, Prompt: "Tell me about love", - }, - Context: QueryContext{ - History: []string{"Previous query"}, - Schema: "custom", - Verses: []string{"Gen 1:1"}, - Words: []string{"Create"}, - User: UserContext{ - Version: "NIV", + Context: QueryContext{ + History: []string{"Previous query"}, + Schema: "custom", + Verses: []string{"Gen 1:1"}, + Words: []string{"Create"}, }, }, + User: UserOptions{ + Version: "NIV", + }, } data, err := json.Marshal(req) @@ -78,7 +78,35 @@ func TestWordSearchResponse_Unmarshal(t *testing.T) { } } +func TestPromptResponse_Unmarshal(t *testing.T) { + jsonStr := `{ + "data": { + "text": "God is love.", + "references": [{"verse": "1 John 4:8", "url": "http://bible.com"}] + }, + "meta": { + "ai_provider": "openai" + } + }` + var resp PromptResponse + err := json.Unmarshal([]byte(jsonStr), &resp) + if err != nil { + t.Fatalf("Failed to unmarshal PromptResponse: %v", err) + } + + if resp.Data.Text != "God is love." { + t.Errorf("Expected text 'God is love.', got '%s'", resp.Data.Text) + } + if len(resp.Data.References) != 1 { + t.Errorf("Expected 1 reference, got %d", len(resp.Data.References)) + } + if resp.Meta.AIProvider != "openai" { + t.Errorf("Expected AI provider 'openai', got '%s'", resp.Meta.AIProvider) + } +} + func TestOQueryResponse_Unmarshal(t *testing.T) { + // This tests direct unmarshal of OQueryResponse, which is still used internally or if schema matches jsonStr := `{ "text": "God is love.", "references": [{"verse": "1 John 4:8", "url": "http://bible.com"}] diff --git a/pkg/app/ask.go b/pkg/app/ask.go index 203cf46..bf25211 100644 --- a/pkg/app/ask.go +++ b/pkg/app/ask.go @@ -33,16 +33,16 @@ func GetBibleAskWithContext(env def.SessionData, contextVerses []string) def.Ses req := QueryRequest{ Query: QueryObject{ Prompt: env.Msg.Message, - }, - Context: QueryContext{ - User: UserContext{ - Version: config.Version, + Context: QueryContext{ + Verses: contextVerses, }, - Verses: contextVerses, + }, + User: UserOptions{ + Version: config.Version, }, } - var resp OQueryResponse + var resp PromptResponse err := SubmitQuery(req, &resp) if err != nil { log.Printf("Error asking bible: %v", err) @@ -51,11 +51,11 @@ func GetBibleAskWithContext(env def.SessionData, contextVerses []string) def.Ses } var sb strings.Builder - sb.WriteString(ParseToTelegramHTML(resp.Text)) + sb.WriteString(ParseToTelegramHTML(resp.Data.Text)) - if len(resp.References) > 0 { + if len(resp.Data.References) > 0 { sb.WriteString("\n\nReferences:") - for _, ref := range resp.References { + for _, ref := range resp.Data.References { sb.WriteString(fmt.Sprintf("\n• %s", stdhtml.EscapeString(ref.Verse))) } } diff --git a/pkg/app/ask_test.go b/pkg/app/ask_test.go index c88fd1f..a4709df 100644 --- a/pkg/app/ask_test.go +++ b/pkg/app/ask_test.go @@ -66,11 +66,11 @@ func TestGetBibleAsk(t *testing.T) { if capturedReq.Query.Prompt != "Explain this" { t.Errorf("Expected Query.Prompt to be 'Explain this', got '%s'", capturedReq.Query.Prompt) } - if len(capturedReq.Context.Verses) != 2 { - t.Errorf("Expected Context.Verses to have 2 items, got %v", capturedReq.Context.Verses) + if len(capturedReq.Query.Context.Verses) != 2 { + t.Errorf("Expected Context.Verses to have 2 items, got %v", capturedReq.Query.Context.Verses) } - if capturedReq.Context.Verses[0] != "John 3:16" { - t.Errorf("Expected Context.Verses[0] to be 'John 3:16', got '%s'", capturedReq.Context.Verses[0]) + if capturedReq.Query.Context.Verses[0] != "John 3:16" { + t.Errorf("Expected Context.Verses[0] to be 'John 3:16', got '%s'", capturedReq.Query.Context.Verses[0]) } }) @@ -123,11 +123,13 @@ func TestGetBibleAsk(t *testing.T) { // Mock SubmitQuery to return HTML SubmitQuery = func(req QueryRequest, result interface{}) error { - if r, ok := result.(*OQueryResponse); ok { - *r = OQueryResponse{ - Text: "
God is Love
", - References: []SearchResult{ - {Verse: "1 John 4:8"}, + if r, ok := result.(*PromptResponse); ok { + *r = PromptResponse{ + Data: OQueryResponse{ + Text: "God is Love
", + References: []SearchResult{ + {Verse: "1 John 4:8"}, + }, }, } } diff --git a/pkg/app/passage.go b/pkg/app/passage.go index 223baf6..c6e4010 100644 --- a/pkg/app/passage.go +++ b/pkg/app/passage.go @@ -240,10 +240,8 @@ func GetBiblePassage(env def.SessionData) def.SessionData { Query: QueryObject{ Verses: []string{env.Msg.Message}, }, - Context: QueryContext{ - User: UserContext{ - Version: config.Version, - }, + User: UserOptions{ + Version: config.Version, }, } diff --git a/pkg/app/passage_test.go b/pkg/app/passage_test.go index 29fdaeb..ad7c546 100644 --- a/pkg/app/passage_test.go +++ b/pkg/app/passage_test.go @@ -93,6 +93,9 @@ func TestGetBiblePassage(t *testing.T) { if capturedReq.Query.Prompt != "" { t.Errorf("Expected Query.Prompt to be empty, got '%s'", capturedReq.Query.Prompt) } + if capturedReq.User.Version != "NIV" { + t.Errorf("Expected User.Version to be 'NIV', got '%s'", capturedReq.User.Version) + } }) t.Run("Success: Response", func(t *testing.T) { diff --git a/pkg/app/search.go b/pkg/app/search.go index e591891..83419b9 100644 --- a/pkg/app/search.go +++ b/pkg/app/search.go @@ -30,10 +30,8 @@ func GetBibleSearch(env def.SessionData) def.SessionData { Query: QueryObject{ Words: words, }, - Context: QueryContext{ - User: UserContext{ - Version: config.Version, - }, + User: UserOptions{ + Version: config.Version, }, } diff --git a/pkg/app/search_test.go b/pkg/app/search_test.go index ba15b39..9e933df 100644 --- a/pkg/app/search_test.go +++ b/pkg/app/search_test.go @@ -49,6 +49,9 @@ func TestGetBibleSearch(t *testing.T) { if capturedReq.Query.Prompt != "" { t.Errorf("Expected Query.Prompt to be empty, got '%s'", capturedReq.Query.Prompt) } + if capturedReq.User.Version != "NIV" { + t.Errorf("Expected User.Version to be 'NIV', got '%s'", capturedReq.User.Version) + } }) t.Run("Success: Response", func(t *testing.T) { diff --git a/pkg/app/test_utils_mock.go b/pkg/app/test_utils_mock.go index 0c41a90..305e69a 100644 --- a/pkg/app/test_utils_mock.go +++ b/pkg/app/test_utils_mock.go @@ -19,6 +19,16 @@ func MockSubmitQuery(t HelperT, callback func(QueryRequest)) func(QueryRequest, {Verse: "John 3:16", URL: "https://example.com/John3:16"}, }, } + case *PromptResponse: + *r = PromptResponse{ + Data: OQueryResponse{ + Text: "This is a mock response.", + References: []SearchResult{ + {Verse: "John 3:16", URL: "https://example.com/John3:16"}, + }, + }, + Meta: Meta{AIProvider: "mock"}, + } case *VerseResponse: *r = VerseResponse{ Verse: "For God so loved the world...",