Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions pkg/app/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"net/url"
"sync"

"github.com/julwrites/ScriptureBot/pkg/secrets"
Expand Down Expand Up @@ -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...",
Expand Down Expand Up @@ -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
}
31 changes: 30 additions & 1 deletion pkg/app/api_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
}
})
}
53 changes: 41 additions & 12 deletions pkg/app/api_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
}
Expand All @@ -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"`
}
44 changes: 36 additions & 8 deletions pkg/app/api_models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"}]
Expand Down
18 changes: 9 additions & 9 deletions pkg/app/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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\n<b>References:</b>")
for _, ref := range resp.References {
for _, ref := range resp.Data.References {
sb.WriteString(fmt.Sprintf("\n• %s", stdhtml.EscapeString(ref.Verse)))
}
}
Expand Down
20 changes: 11 additions & 9 deletions pkg/app/ask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
})

Expand Down Expand Up @@ -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: "<p>God is <b>Love</b></p>",
References: []SearchResult{
{Verse: "1 John 4:8"},
if r, ok := result.(*PromptResponse); ok {
*r = PromptResponse{
Data: OQueryResponse{
Text: "<p>God is <b>Love</b></p>",
References: []SearchResult{
{Verse: "1 John 4:8"},
},
},
}
}
Expand Down
6 changes: 2 additions & 4 deletions pkg/app/passage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/app/passage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading