feat: add OpenBrain and EaaS scoring API client methods
Brain operations (remember, recall, forget, ensureCollection) and EaaS scoring (content, imprint, health) on the existing Client. Maps to api.lthn.ai/v1/brain/* and api.lthn.ai/v1/score/* endpoints served by php-agentic's BrainApiController and ScoreApiController. 20 new tests, all passing. Full suite green. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b77c590f61
commit
deb7021b93
4 changed files with 762 additions and 0 deletions
215
brain.go
Normal file
215
brain.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/log"
|
||||
)
|
||||
|
||||
// MemoryType represents the classification of a brain memory.
|
||||
type MemoryType string
|
||||
|
||||
const (
|
||||
MemoryFact MemoryType = "fact"
|
||||
MemoryDecision MemoryType = "decision"
|
||||
MemoryPattern MemoryType = "pattern"
|
||||
MemoryContext MemoryType = "context"
|
||||
MemoryProcedure MemoryType = "procedure"
|
||||
)
|
||||
|
||||
// Memory represents a single memory entry from the OpenBrain API.
|
||||
type Memory struct {
|
||||
ID string `json:"id"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Project string `json:"project,omitempty"`
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
SupersedesID string `json:"supersedes_id,omitempty"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// RememberRequest is the payload for storing a new memory.
|
||||
type RememberRequest struct {
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Project string `json:"project,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
SupersedesID string `json:"supersedes_id,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// RememberResponse is returned after storing a memory.
|
||||
type RememberResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Project string `json:"project"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// RecallRequest is the payload for semantic search.
|
||||
type RecallRequest struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
Project string `json:"project,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
MinConfidence *float64 `json:"min_confidence,omitempty"`
|
||||
}
|
||||
|
||||
// RecallResponse is returned from a semantic search.
|
||||
type RecallResponse struct {
|
||||
Memories []Memory `json:"memories"`
|
||||
Scores map[string]float64 `json:"scores"`
|
||||
}
|
||||
|
||||
// Remember stores a memory via POST /v1/brain/remember.
|
||||
func (c *Client) Remember(ctx context.Context, req RememberRequest) (*RememberResponse, error) {
|
||||
const op = "agentic.Client.Remember"
|
||||
|
||||
if req.Content == "" {
|
||||
return nil, log.E(op, "content is required", nil)
|
||||
}
|
||||
if req.Type == "" {
|
||||
return nil, log.E(op, "type is required", nil)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to marshal request", err)
|
||||
}
|
||||
|
||||
endpoint := c.BaseURL + "/v1/brain/remember"
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
c.setHeaders(httpReq)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return nil, log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
var result RememberResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, log.E(op, "failed to decode response", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Recall performs semantic search via POST /v1/brain/recall.
|
||||
func (c *Client) Recall(ctx context.Context, req RecallRequest) (*RecallResponse, error) {
|
||||
const op = "agentic.Client.Recall"
|
||||
|
||||
if req.Query == "" {
|
||||
return nil, log.E(op, "query is required", nil)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to marshal request", err)
|
||||
}
|
||||
|
||||
endpoint := c.BaseURL + "/v1/brain/recall"
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
c.setHeaders(httpReq)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return nil, log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
var result RecallResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, log.E(op, "failed to decode response", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Forget removes a memory via DELETE /v1/brain/forget/{id}.
|
||||
func (c *Client) Forget(ctx context.Context, id string) error {
|
||||
const op = "agentic.Client.Forget"
|
||||
|
||||
if id == "" {
|
||||
return log.E(op, "memory ID is required", nil)
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s/v1/brain/forget/%s", c.BaseURL, url.PathEscape(id))
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
c.setHeaders(httpReq)
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureCollection ensures the Qdrant collection exists via POST /v1/brain/collections.
|
||||
func (c *Client) EnsureCollection(ctx context.Context) error {
|
||||
const op = "agentic.Client.EnsureCollection"
|
||||
|
||||
endpoint := c.BaseURL + "/v1/brain/collections"
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
|
||||
if err != nil {
|
||||
return log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
c.setHeaders(httpReq)
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
234
brain_test.go
Normal file
234
brain_test.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_Remember_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/v1/brain/remember", r.URL.Path)
|
||||
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
var req RememberRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Go uses structural typing", req.Content)
|
||||
assert.Equal(t, "fact", req.Type)
|
||||
assert.Equal(t, "go-agentic", req.Project)
|
||||
assert.Equal(t, []string{"go", "typing"}, req.Tags)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(RememberResponse{
|
||||
ID: "mem-abc-123",
|
||||
Type: "fact",
|
||||
Project: "go-agentic",
|
||||
CreatedAt: "2026-03-03T12:00:00+00:00",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.Remember(context.Background(), RememberRequest{
|
||||
Content: "Go uses structural typing",
|
||||
Type: "fact",
|
||||
Project: "go-agentic",
|
||||
Tags: []string{"go", "typing"},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "mem-abc-123", result.ID)
|
||||
assert.Equal(t, "fact", result.Type)
|
||||
assert.Equal(t, "go-agentic", result.Project)
|
||||
}
|
||||
|
||||
func TestClient_Remember_Bad_EmptyContent(t *testing.T) {
|
||||
client := NewClient("https://api.example.com", "test-token")
|
||||
result, err := client.Remember(context.Background(), RememberRequest{
|
||||
Type: "fact",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "content is required")
|
||||
}
|
||||
|
||||
func TestClient_Remember_Bad_EmptyType(t *testing.T) {
|
||||
client := NewClient("https://api.example.com", "test-token")
|
||||
result, err := client.Remember(context.Background(), RememberRequest{
|
||||
Content: "something",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "type is required")
|
||||
}
|
||||
|
||||
func TestClient_Remember_Bad_ServerError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
_ = json.NewEncoder(w).Encode(APIError{Message: "validation failed"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.Remember(context.Background(), RememberRequest{
|
||||
Content: "test",
|
||||
Type: "fact",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "validation failed")
|
||||
}
|
||||
|
||||
func TestClient_Recall_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/v1/brain/recall", r.URL.Path)
|
||||
|
||||
var req RecallRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "how does typing work in Go", req.Query)
|
||||
assert.Equal(t, 5, req.TopK)
|
||||
assert.Equal(t, "go-agentic", req.Project)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(RecallResponse{
|
||||
Memories: []Memory{
|
||||
{
|
||||
ID: "mem-abc-123",
|
||||
Type: "fact",
|
||||
Content: "Go uses structural typing",
|
||||
Project: "go-agentic",
|
||||
Confidence: 0.95,
|
||||
},
|
||||
},
|
||||
Scores: map[string]float64{
|
||||
"mem-abc-123": 0.87,
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.Recall(context.Background(), RecallRequest{
|
||||
Query: "how does typing work in Go",
|
||||
TopK: 5,
|
||||
Project: "go-agentic",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.Memories, 1)
|
||||
assert.Equal(t, "mem-abc-123", result.Memories[0].ID)
|
||||
assert.Equal(t, "Go uses structural typing", result.Memories[0].Content)
|
||||
assert.InDelta(t, 0.87, result.Scores["mem-abc-123"], 0.001)
|
||||
}
|
||||
|
||||
func TestClient_Recall_Good_EmptyResults(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(RecallResponse{
|
||||
Memories: []Memory{},
|
||||
Scores: map[string]float64{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.Recall(context.Background(), RecallRequest{
|
||||
Query: "something obscure",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result.Memories)
|
||||
assert.Empty(t, result.Scores)
|
||||
}
|
||||
|
||||
func TestClient_Recall_Bad_EmptyQuery(t *testing.T) {
|
||||
client := NewClient("https://api.example.com", "test-token")
|
||||
result, err := client.Recall(context.Background(), RecallRequest{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "query is required")
|
||||
}
|
||||
|
||||
func TestClient_Forget_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodDelete, r.Method)
|
||||
assert.Equal(t, "/v1/brain/forget/mem-abc-123", r.URL.Path)
|
||||
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"deleted": true})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
err := client.Forget(context.Background(), "mem-abc-123")
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_Forget_Bad_EmptyID(t *testing.T) {
|
||||
client := NewClient("https://api.example.com", "test-token")
|
||||
err := client.Forget(context.Background(), "")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "memory ID is required")
|
||||
}
|
||||
|
||||
func TestClient_Forget_Bad_NotFound(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_ = json.NewEncoder(w).Encode(APIError{Message: "memory not found"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
err := client.Forget(context.Background(), "nonexistent")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "memory not found")
|
||||
}
|
||||
|
||||
func TestClient_EnsureCollection_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/v1/brain/collections", r.URL.Path)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
err := client.EnsureCollection(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_EnsureCollection_Bad_ServerError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_ = json.NewEncoder(w).Encode(APIError{Message: "collection setup failed"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
err := client.EnsureCollection(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "collection setup failed")
|
||||
}
|
||||
147
score.go
Normal file
147
score.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/log"
|
||||
)
|
||||
|
||||
// ScoreContentRequest is the payload for content scoring.
|
||||
type ScoreContentRequest struct {
|
||||
Text string `json:"text"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
}
|
||||
|
||||
// ScoreImprintRequest is the payload for linguistic imprint analysis.
|
||||
type ScoreImprintRequest struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// ScoreResult holds the response from the scoring engine.
|
||||
// The shape is proxied from the EaaS Go binary, so fields are dynamic.
|
||||
type ScoreResult map[string]any
|
||||
|
||||
// ScoreHealthResponse holds the health check result.
|
||||
type ScoreHealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
UpstreamStatus int `json:"upstream_status,omitempty"`
|
||||
}
|
||||
|
||||
// ScoreContent scores text for AI patterns via POST /v1/score/content.
|
||||
func (c *Client) ScoreContent(ctx context.Context, req ScoreContentRequest) (ScoreResult, error) {
|
||||
const op = "agentic.Client.ScoreContent"
|
||||
|
||||
if req.Text == "" {
|
||||
return nil, log.E(op, "text is required", nil)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to marshal request", err)
|
||||
}
|
||||
|
||||
endpoint := c.BaseURL + "/v1/score/content"
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
c.setHeaders(httpReq)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return nil, log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
var result ScoreResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, log.E(op, "failed to decode response", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ScoreImprint performs linguistic imprint analysis via POST /v1/score/imprint.
|
||||
func (c *Client) ScoreImprint(ctx context.Context, req ScoreImprintRequest) (ScoreResult, error) {
|
||||
const op = "agentic.Client.ScoreImprint"
|
||||
|
||||
if req.Text == "" {
|
||||
return nil, log.E(op, "text is required", nil)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to marshal request", err)
|
||||
}
|
||||
|
||||
endpoint := c.BaseURL + "/v1/score/imprint"
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
c.setHeaders(httpReq)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return nil, log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
var result ScoreResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, log.E(op, "failed to decode response", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ScoreHealth checks the scoring engine health via GET /v1/score/health.
|
||||
// This endpoint does not require authentication.
|
||||
func (c *Client) ScoreHealth(ctx context.Context) (*ScoreHealthResponse, error) {
|
||||
const op = "agentic.Client.ScoreHealth"
|
||||
|
||||
endpoint := c.BaseURL + "/v1/score/health"
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "failed to create request", err)
|
||||
}
|
||||
|
||||
// Health endpoint is unauthenticated but we still set headers for consistency.
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
httpReq.Header.Set("User-Agent", "core-agentic-client/1.0")
|
||||
|
||||
resp, err := c.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, log.E(op, "request failed", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := c.checkResponse(resp); err != nil {
|
||||
return nil, log.E(op, "API error", err)
|
||||
}
|
||||
|
||||
var result ScoreHealthResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, log.E(op, "failed to decode response", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
166
score_test.go
Normal file
166
score_test.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
package agentic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_ScoreContent_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/v1/score/content", r.URL.Path)
|
||||
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
||||
|
||||
var req ScoreContentRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, req.Text, "sample text for scoring")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"score": 0.23,
|
||||
"confidence": 0.91,
|
||||
"label": "human",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.ScoreContent(context.Background(), ScoreContentRequest{
|
||||
Text: "This is some sample text for scoring that is at least twenty characters",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.InDelta(t, 0.23, result["score"], 0.001)
|
||||
assert.Equal(t, "human", result["label"])
|
||||
}
|
||||
|
||||
func TestClient_ScoreContent_Good_WithPrompt(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ScoreContentRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Check for formality", req.Prompt)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"score": 0.5})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.ScoreContent(context.Background(), ScoreContentRequest{
|
||||
Text: "This text should be checked for formality and style patterns",
|
||||
Prompt: "Check for formality",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.InDelta(t, 0.5, result["score"], 0.001)
|
||||
}
|
||||
|
||||
func TestClient_ScoreContent_Bad_EmptyText(t *testing.T) {
|
||||
client := NewClient("https://api.example.com", "test-token")
|
||||
result, err := client.ScoreContent(context.Background(), ScoreContentRequest{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "text is required")
|
||||
}
|
||||
|
||||
func TestClient_ScoreContent_Bad_ServiceDown(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"error": "scoring_unavailable",
|
||||
"message": "Could not reach the scoring service.",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.ScoreContent(context.Background(), ScoreContentRequest{
|
||||
Text: "This text needs at least twenty characters to validate",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestClient_ScoreImprint_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/v1/score/imprint", r.URL.Path)
|
||||
|
||||
var req ScoreImprintRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, req.Text)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"imprint": "abc123def456",
|
||||
"confidence": 0.88,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.ScoreImprint(context.Background(), ScoreImprintRequest{
|
||||
Text: "This text has a distinct linguistic imprint pattern to analyse",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "abc123def456", result["imprint"])
|
||||
}
|
||||
|
||||
func TestClient_ScoreImprint_Bad_EmptyText(t *testing.T) {
|
||||
client := NewClient("https://api.example.com", "test-token")
|
||||
result, err := client.ScoreImprint(context.Background(), ScoreImprintRequest{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "text is required")
|
||||
}
|
||||
|
||||
func TestClient_ScoreHealth_Good(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodGet, r.Method)
|
||||
assert.Equal(t, "/v1/score/health", r.URL.Path)
|
||||
// Health endpoint should not require auth token
|
||||
assert.Empty(t, r.Header.Get("Authorization"))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(ScoreHealthResponse{
|
||||
Status: "healthy",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.ScoreHealth(context.Background())
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "healthy", result.Status)
|
||||
}
|
||||
|
||||
func TestClient_ScoreHealth_Bad_Unhealthy(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_ = json.NewEncoder(w).Encode(ScoreHealthResponse{
|
||||
Status: "unhealthy",
|
||||
UpstreamStatus: 503,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(server.URL, "test-token")
|
||||
result, err := client.ScoreHealth(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
Reference in a new issue