client.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. package hyper
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "time"
  11. "github.com/google/uuid"
  12. fantasy "charm.land/fantasy"
  13. )
  14. // Client is a minimal client for the Hyper API.
  15. type Client struct {
  16. BaseURL *url.URL
  17. APIKey string
  18. HTTPClient *http.Client
  19. }
  20. // New creates a new Hyper client.
  21. func New(base string, apiKey string) (*Client, error) {
  22. u, err := url.Parse(strings.TrimRight(base, "/"))
  23. if err != nil {
  24. return nil, fmt.Errorf("parse base url: %w", err)
  25. }
  26. return &Client{
  27. BaseURL: u,
  28. APIKey: apiKey,
  29. HTTPClient: &http.Client{Timeout: 30 * time.Second},
  30. }, nil
  31. }
  32. // Project mirrors the JSON returned by Hyper.
  33. type Project struct {
  34. ID uuid.UUID `json:"id"`
  35. Name string `json:"name"`
  36. Description string `json:"description"`
  37. OrganizationID uuid.UUID `json:"organization_id"`
  38. UserID uuid.UUID `json:"user_id"`
  39. Archived bool `json:"archived"`
  40. Identifiers []string `json:"identifiers"`
  41. CreatedAt time.Time `json:"created_at"`
  42. UpdatedAt time.Time `json:"updated_at"`
  43. }
  44. // CreateProject creates a project.
  45. func (c *Client) CreateProject(ctx context.Context, name, description string, organizationID uuid.UUID, identifiers []string) (Project, error) {
  46. var p Project
  47. body := map[string]any{
  48. "name": name,
  49. "description": description,
  50. "organization_id": organizationID,
  51. }
  52. if len(identifiers) > 0 {
  53. body["identifiers"] = identifiers
  54. }
  55. bts, _ := json.Marshal(body)
  56. endpoint := c.BaseURL.JoinPath("api/v1", "projects").String()
  57. req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(bts))
  58. if err != nil {
  59. return p, err
  60. }
  61. c.addAuth(req)
  62. req.Header.Set("Content-Type", "application/json")
  63. resp, err := c.http().Do(req)
  64. if err != nil {
  65. return p, err
  66. }
  67. defer resp.Body.Close()
  68. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  69. return p, fmt.Errorf("create project: http %d", resp.StatusCode)
  70. }
  71. if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
  72. return p, err
  73. }
  74. return p, nil
  75. }
  76. // ListProjects lists projects for the authenticated user.
  77. // If identifiers is not empty, projects that match ANY identifier are returned.
  78. func (c *Client) ListProjects(ctx context.Context, identifiers []string) ([]Project, error) {
  79. endpoint := c.BaseURL.JoinPath("api/v1", "projects")
  80. q := endpoint.Query()
  81. if len(identifiers) > 0 {
  82. q.Set("identifiers", strings.Join(identifiers, ","))
  83. endpoint.RawQuery = q.Encode()
  84. }
  85. req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
  86. if err != nil {
  87. return nil, err
  88. }
  89. c.addAuth(req)
  90. resp, err := c.http().Do(req)
  91. if err != nil {
  92. return nil, err
  93. }
  94. defer resp.Body.Close()
  95. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  96. return nil, fmt.Errorf("list projects: http %d", resp.StatusCode)
  97. }
  98. var ps []Project
  99. if err := json.NewDecoder(resp.Body).Decode(&ps); err != nil {
  100. return nil, err
  101. }
  102. return ps, nil
  103. }
  104. // Memorize sends messages to be memorized for a given project and echoes them back.
  105. func (c *Client) Memorize(ctx context.Context, projectID uuid.UUID, msgs []fantasy.Message) ([]fantasy.Message, error) {
  106. bts, _ := json.Marshal(msgs)
  107. endpoint := c.BaseURL.JoinPath("api/v1", "projects", projectID.String(), "memorize").String()
  108. req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(bts))
  109. if err != nil {
  110. return nil, err
  111. }
  112. c.addAuth(req)
  113. req.Header.Set("Content-Type", "application/json")
  114. resp, err := c.http().Do(req)
  115. if err != nil {
  116. return nil, err
  117. }
  118. defer resp.Body.Close()
  119. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  120. return nil, fmt.Errorf("memorize: http %d", resp.StatusCode)
  121. }
  122. var echoed []fantasy.Message
  123. if err := json.NewDecoder(resp.Body).Decode(&echoed); err != nil {
  124. return nil, err
  125. }
  126. return echoed, nil
  127. }
  128. // ProjectMemories fetches memory bullets for a project using optional query, type filters and limit.
  129. func (c *Client) ProjectMemories(ctx context.Context, projectID uuid.UUID, query string, types []string, limit int) ([]string, error) {
  130. endpoint := c.BaseURL.JoinPath("api/v1", "projects", projectID.String(), "memories")
  131. q := endpoint.Query()
  132. if strings.TrimSpace(query) != "" {
  133. q.Set("q", query)
  134. }
  135. for _, t := range types {
  136. if strings.TrimSpace(t) != "" {
  137. q.Add("type", t)
  138. }
  139. }
  140. if limit > 0 {
  141. q.Set("limit", fmt.Sprintf("%d", limit))
  142. }
  143. endpoint.RawQuery = q.Encode()
  144. req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
  145. if err != nil {
  146. return nil, err
  147. }
  148. c.addAuth(req)
  149. resp, err := c.http().Do(req)
  150. if err != nil {
  151. return nil, err
  152. }
  153. defer resp.Body.Close()
  154. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  155. return nil, fmt.Errorf("project memories: http %d", resp.StatusCode)
  156. }
  157. var bullets []string
  158. if err := json.NewDecoder(resp.Body).Decode(&bullets); err != nil {
  159. return nil, err
  160. }
  161. return bullets, nil
  162. }
  163. func (c *Client) http() *http.Client {
  164. if c.HTTPClient != nil {
  165. return c.HTTPClient
  166. }
  167. return http.DefaultClient
  168. }
  169. func (c *Client) addAuth(req *http.Request) {
  170. if c.APIKey != "" {
  171. req.Header.Set("Authorization", "Bearer "+c.APIKey)
  172. }
  173. }