Skip to content
Open
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
83 changes: 80 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A Go client library for interacting with Dappier's API's.
To install the package, run:

```bash
go get github.com/yourusername/dappier-go
go get github.com/dappier/dappier-go

```

Expand All @@ -29,7 +29,7 @@ import (
"fmt"
"log"

"github.com/DappierAI/dappier-go"
"github.com/dappier/dappier-go"
)

func main() {
Expand Down Expand Up @@ -63,7 +63,7 @@ import (
"fmt"
"log"

"github.com/DappierAI/dappier-go"
"github.com/dappier/dappier-go"
)

func main() {
Expand Down Expand Up @@ -139,3 +139,80 @@ func main() {
1. Replace `/datamodel/[DATAMODEL_ID]` with the appropriate Data Model ID from your Data Model API for the respective AI Agent.
2. Replace the Bearer token in the Authorization header with your Dappier API key. This can be retrieved from the user profile settings.


## Search API Usage

The Search API allows you to query with different data_model_ids, enabling flexible data search across different models.

Here’s how to use the Search API:

```go
package main

import (
"fmt"
"log"
"github.com/dappier/dappier-go"
)

func main() {
// Initialize Dappier client
client, err := dappier.NewDappierApp("your-api-key")
if err != nil {
log.Fatalf("Failed to initialize Dappier client: %v", err)
}

// Example: Search with default options
searchResult, err := client.Search("Latest Microsoft News", "dm_01htjq2njgecvah7ncepm8v87y")
if err != nil {
log.Fatalf("Failed to get search results: %v", err)
}

// Print the results
for _, result := range searchResult.Response.Results {
fmt.Printf("Search Result - %v\n", result)
}

// Example: Search with custom options
searchCustomResult, err := client.Search(
"Latest Microsoft News",
"dm_01htjq2njgecvah7ncepm8v87y",
dappier.WithSearchSimilarityTopK(6), // Custom similarity_top_k
dappier.WithSearchRef("familyproof.com"), // Custom ref
dappier.WithSearchNumArticlesRef(3), // Ensure 3 articles from the domain
)
if err != nil {
log.Fatalf("Failed to get search results: %v", err)
}

// Print the custom results
for _, result := range searchCustomResult.Response.Results {
fmt.Printf("Search Result (Custom) - %v\n", result)
}
}


```
Comment on lines +143 to +195
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Approve addition of Search API Usage section with minor suggestion

The new "Search API Usage" section is a valuable addition to the documentation. It provides clear explanations and code examples for both default and custom usage of the Search API. The structure and style are consistent with the existing "AI Recommendations API Usage" section, which is excellent for maintaining documentation coherence.

To further improve clarity, consider adding a brief explanation of the data_model_id parameter in the introductory paragraph or as a comment in the code example. This would help users understand the significance of this parameter when using the Search API.

Comment on lines +149 to +195
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Approve code examples with suggestion for consistency

The code examples for the Search API Usage are clear, demonstrative, and follow good practices such as proper error handling. They effectively show how to use both default and custom options when making Search API requests.

For consistency with the AI Recommendations API example, consider adding a brief comment explaining each custom option in the Search API example. This would help users understand the purpose of each parameter more easily.

Example of suggested addition:

searchCustomResult, err := client.Search(
  "Latest Microsoft News",
  "dm_01htjq2njgecvah7ncepm8v87y",
  dappier.WithSearchSimilarityTopK(6),    // Set custom number of results to return
  dappier.WithSearchRef("familyproof.com"), // Set custom reference domain
  dappier.WithSearchNumArticlesRef(3),    // Ensure 3 articles from the reference domain
)


## Parameters

### `query` (string):
- A natural language query or URL.
- If a URL is passed, the AI analyzes the page, creates a summary, and performs a semantic search query based on the content.

### `similarity_top_k` (integer):
- The number of articles to return. Default is 9.

### `ref` (string):
- The domain of the site from which the recommendations should come.
- Example: `techcrunch.com`.

### `num_articles_ref` (integer):
- Specifies how many articles should be guaranteed to match the domain specified in `ref`.
- Use this to ensure a set number of articles from the desired domain appear in the results.

### `search_algorithm` (string):
- Options: `"most_recent"` or `"semantic"`.
- `"semantic"` (default): Contextual matching of the query to retrieve articles.
- `"most_recent"`: Retrieves articles sorted by the most recent publication date.

Comment on lines +197 to +218
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Improve formatting of Search API Parameters section

The Parameters section for the Search API provides clear and consistent descriptions with the AI Recommendations API section. However, to maintain visual consistency throughout the document, consider applying the following formatting improvements:

  1. Add a blank line before and after each parameter heading.
  2. Remove the colon (:) from the end of each parameter heading.
  3. Ensure consistent indentation for list items under each parameter.

Here's an example of the suggested formatting:

## Parameters

### `query` (string)

- A natural language query or URL.
- If a URL is passed, the AI analyzes the page, creates a summary, and performs a semantic search query based on the content.

### `similarity_top_k` (integer)

- The number of articles to return. Default is 9.

...
🧰 Tools
🪛 LanguageTool

[uncategorized] ~204-~204: A determiner appears to be missing. Consider inserting it.
Context: ...r): - The number of articles to return. Default is 9. ### ref (string): - The domain...

(AI_EN_LECTOR_MISSING_DETERMINER)


[uncategorized] ~217-~217: Loose punctuation mark.
Context: ...o retrieve articles. - "most_recent": Retrieves articles sorted by the most r...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 Markdownlint

199-199: Expected: 1; Actual: 0; Below
Headings should be surrounded by blank lines

(MD022, blanks-around-headings)


203-203: Expected: 1; Actual: 0; Below
Headings should be surrounded by blank lines

(MD022, blanks-around-headings)


206-206: Expected: 1; Actual: 0; Below
Headings should be surrounded by blank lines

(MD022, blanks-around-headings)


210-210: Expected: 1; Actual: 0; Below
Headings should be surrounded by blank lines

(MD022, blanks-around-headings)


214-214: Expected: 1; Actual: 0; Below
Headings should be surrounded by blank lines

(MD022, blanks-around-headings)


197-197: null
Multiple headings with the same content

(MD024, no-duplicate-heading)


199-199: Punctuation: ':'
Trailing punctuation in heading

(MD026, no-trailing-punctuation)


203-203: Punctuation: ':'
Trailing punctuation in heading

(MD026, no-trailing-punctuation)


206-206: Punctuation: ':'
Trailing punctuation in heading

(MD026, no-trailing-punctuation)


210-210: Punctuation: ':'
Trailing punctuation in heading

(MD026, no-trailing-punctuation)


214-214: Punctuation: ':'
Trailing punctuation in heading

(MD026, no-trailing-punctuation)


200-200: null
Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


204-204: null
Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


207-207: null
Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


211-211: null
Lists should be surrounded by blank lines

(MD032, blanks-around-lists)


215-215: null
Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

118 changes: 111 additions & 7 deletions dappier.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@ import (

const (
BaseUrl = "https://api.dappier.com/app/datamodel"
SearchBaseUrl = "https://api.dappier.com/app/v2/search"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consistency in Constant Naming

Consider renaming SearchBaseUrl to SearchBaseURL to maintain consistent camel casing with BaseUrl and improve code readability.

RealtimeDataModelId = "dm_01hpsxyfm2fwdt2zet9cg6fdxt"
ContentType = "application/json"
)

// AiRecommendationsRequest represents the request payload structure for the Dappier AI recommendations API
type AiRecommendationsRequest struct {
// SearchRequest represents the request payload structure for the Dappier AI recommendations API
type SearchRequest struct {
Query string `json:"query"` // Natural language query or URL
SimilarityTopK int `json:"similarity_top_k"` // The number of articles to return (default is 9)
Ref string `json:"ref"` // Domain from which to fetch recommendations (e.g., techcrunch.com)
NumArticlesRef int `json:"num_articles_ref"` // Guaranteed number of articles from the specified domain
}
Comment on lines +19 to 25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clarify the Purpose of SearchRequest Struct

The SearchRequest struct is introduced for the search functionality. However, it's currently being used in both the Search and AIRecommendations methods. To enhance code clarity and maintainability, consider creating separate structs for each API endpoint if their payloads differ or if they serve different purposes.


type SearchResponse struct {
Status string `json:"status"`
Response SearchResult `json:"response"`
}

type SearchResult struct {
Query string `json:"query"`
Results []map[string]interface{} `json:"results"` // Dynamic JSON structure
Message string `json:"message"`
}
Comment on lines +28 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Define Concrete Types for Search Results

Using map[string]interface{} for Results in SearchResult can lead to type assertions and potential runtime errors. Define a concrete struct for the search results to improve type safety and make the code more maintainable.

Example:

type SearchResult struct {
	Query   string        `json:"query"`
-	Results []map[string]interface{} `json:"results"` // Dynamic JSON structure
+	Results []SearchItem  `json:"results"`
	Message string        `json:"message"`
}

+type SearchItem struct {
+	Title          string  `json:"title"`
+	URL            string  `json:"url"`
+	Author         string  `json:"author"`
+	ImageURL       string  `json:"image_url"`
+	PreviewContent string  `json:"preview_content"`
+	PubDate        string  `json:"pubdate"`
+	PubDateUnix    int64   `json:"pubdate_unix"`
+	Score          float64 `json:"score"`
+	Site           string  `json:"site"`
+	SiteDomain     string  `json:"site_domain"`
+	// Add other relevant fields
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Status string `json:"status"`
Response SearchResult `json:"response"`
}
type SearchResult struct {
Query string `json:"query"`
Results []map[string]interface{} `json:"results"` // Dynamic JSON structure
Message string `json:"message"`
}
Status string `json:"status"`
Response SearchResult `json:"response"`
}
type SearchResult struct {
Query string `json:"query"`
Results []SearchItem `json:"results"`
Message string `json:"message"`
}
type SearchItem struct {
Title string `json:"title"`
URL string `json:"url"`
Author string `json:"author"`
ImageURL string `json:"image_url"`
PreviewContent string `json:"preview_content"`
PubDate string `json:"pubdate"`
PubDateUnix int64 `json:"pubdate_unix"`
Score float64 `json:"score"`
Site string `json:"site"`
SiteDomain string `json:"site_domain"`
// Add other relevant fields
}


// AiRecommendationsResult represents the response structure for the Dappier AI recommendations API
type AiRecommendationsResult struct {
Results []struct {
Expand Down Expand Up @@ -152,27 +164,27 @@ func (d *DappierApp) RealtimeSearchAPI(query string) (*RealtimeSearchResult, err
}

// RecommendationsOption defines a functional option for configuring the AIRecommendations request
type RecommendationsOption func(*AiRecommendationsRequest)
type RecommendationsOption func(*SearchRequest)

// WithSimilarityTopK sets the similarity_top_k option. The number of articles to return. Default is 9.
func WithSimilarityTopK(k int) RecommendationsOption {
return func(req *AiRecommendationsRequest) {
return func(req *SearchRequest) {
req.SimilarityTopK = k
}
}

// WithRef sets the ref option
// The domain of the site from which the recommendations should come. For example, techcrunch.com.
func WithRef(ref string) RecommendationsOption {
return func(req *AiRecommendationsRequest) {
return func(req *SearchRequest) {
req.Ref = ref
}
}

// WithNumArticlesRef sets the num_articles_ref option
// Specifies how many articles should be guaranteed to match the domain specified in ref.
func WithNumArticlesRef(num int) RecommendationsOption {
return func(req *AiRecommendationsRequest) {
return func(req *SearchRequest) {
Comment on lines +171 to +187
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unify Option Functions for Consistency

There are separate option functions for AIRecommendations (WithSimilarityTopK, WithRef, WithNumArticlesRef) and for Search (WithSearchSimilarityTopK, WithSearchRef, WithSearchNumArticlesRef). To improve usability, consider unifying these functions or clearly differentiating them.

Options:

  • Prefix all option functions with With and the API name, e.g., WithAIRecommendationsSimilarityTopK, WithSearchSimilarityTopK.
  • Alternatively, if the options are the same for both APIs, use a common set of option functions.

Also applies to: 280-298

Comment on lines +167 to +187
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inconsistent Naming of Option Types and Functions

The option type RecommendationsOption and its associated functions (WithSimilarityTopK, WithRef, WithNumArticlesRef) are intended for AIRecommendations but are modifying SearchRequest. Similarly, new option types and functions are introduced for Search. This can cause confusion.

Consider the following:

  • Rename RecommendationsOption to SearchOption if it's modifying SearchRequest.
  • Ensure that option functions are clearly associated with their respective request types.

Example:

-type RecommendationsOption func(*SearchRequest)
+type RecommendationsOption func(*AiRecommendationsRequest)

And update the functions accordingly.

Committable suggestion was skipped due to low confidence.

req.NumArticlesRef = num
}
}
Expand All @@ -194,7 +206,7 @@ func (d *DappierApp) AIRecommendations(query string, datamodelID string, opts ..
}

// Create the request payload
requestData := AiRecommendationsRequest{
requestData := SearchRequest{
Query: query,
SimilarityTopK: 9, // default value for similarityTopK
Ref: "", // default value for ref
Expand Down Expand Up @@ -260,3 +272,95 @@ func (d *DappierApp) AIRecommendations(query string, datamodelID string, opts ..

return &result, nil
}

// SearchOption defines a functional option for configuring the Search request
type SearchOption func(*SearchRequest)

// WithSearchSimilarityTopK sets the similarity_top_k option for the search query
func WithSearchSimilarityTopK(k int) SearchOption {
return func(req *SearchRequest) {
req.SimilarityTopK = k
}
}

// WithSearchRef sets the ref option for the search query
func WithSearchRef(ref string) SearchOption {
return func(req *SearchRequest) {
req.Ref = ref
}
}

// WithSearchNumArticlesRef sets the num_articles_ref option for the search query
func WithSearchNumArticlesRef(num int) SearchOption {
return func(req *SearchRequest) {
req.NumArticlesRef = num
}
}

// Search performs a search on the Dappier API using the provided query and data model ID, with optional parameters
func (d *DappierApp) Search(query string, dataModelID string, opts ...SearchOption) (*SearchResponse, error) {
// Validate inputs
if query == "" || dataModelID == "" {
return nil, errors.New("query and data_model_id cannot be empty")
}

// Create the default request payload with the provided query
requestData := SearchRequest{
Query: query,
SimilarityTopK: 9, // default value
Ref: "", // default value
NumArticlesRef: 0, // default value
}

// Apply all optional parameters to the request data
for _, opt := range opts {
opt(&requestData)
}

// Marshal the request data into JSON
reqBody, err := json.Marshal(requestData)
if err != nil {
return nil, fmt.Errorf("failed to marshal request data: %w", err)
}

// Construct the request URL with the provided data model ID
url := fmt.Sprintf("%s?data_model_id=%s", SearchBaseUrl, dataModelID)

// Create a new HTTP request
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create new request: %w", err)
}

// Set necessary headers
req.Header.Set("Content-Type", ContentType)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.APIKey))

// Use the client's HTTP client to make the request
resp, err := d.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making HTTP request: %w", err)
}
defer resp.Body.Close()

// Check for non-OK status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-OK response status: %d", resp.StatusCode)
}

// Read the response body using io.ReadAll
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

// Unmarshal the response body into the SearchResponse structure
var result SearchResponse
err = json.Unmarshal(body, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}

// Return the result
return &result, nil
}
Comment on lines +301 to +366
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Check for Empty Results in Search Response

After unmarshalling the response in the Search method, there's no check to verify whether Results contains data. If the API returns an empty Results array, it might be appropriate to notify the caller.

Apply this diff to handle empty results:

// Unmarshal the response body into the SearchResponse structure
var result SearchResponse
err = json.Unmarshal(body, &result)
if err != nil {
	return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}

+// Check if results are available
+if len(result.Response.Results) == 0 {
+	return nil, errors.New("no search results found")
+}

return &result, nil
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (d *DappierApp) Search(query string, dataModelID string, opts ...SearchOption) (*SearchResponse, error) {
// Validate inputs
if query == "" || dataModelID == "" {
return nil, errors.New("query and data_model_id cannot be empty")
}
// Create the default request payload with the provided query
requestData := SearchRequest{
Query: query,
SimilarityTopK: 9, // default value
Ref: "", // default value
NumArticlesRef: 0, // default value
}
// Apply all optional parameters to the request data
for _, opt := range opts {
opt(&requestData)
}
// Marshal the request data into JSON
reqBody, err := json.Marshal(requestData)
if err != nil {
return nil, fmt.Errorf("failed to marshal request data: %w", err)
}
// Construct the request URL with the provided data model ID
url := fmt.Sprintf("%s?data_model_id=%s", SearchBaseUrl, dataModelID)
// Create a new HTTP request
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create new request: %w", err)
}
// Set necessary headers
req.Header.Set("Content-Type", ContentType)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.APIKey))
// Use the client's HTTP client to make the request
resp, err := d.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making HTTP request: %w", err)
}
defer resp.Body.Close()
// Check for non-OK status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-OK response status: %d", resp.StatusCode)
}
// Read the response body using io.ReadAll
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Unmarshal the response body into the SearchResponse structure
var result SearchResponse
err = json.Unmarshal(body, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// Return the result
return &result, nil
}
func (d *DappierApp) Search(query string, dataModelID string, opts ...SearchOption) (*SearchResponse, error) {
// Validate inputs
if query == "" || dataModelID == "" {
return nil, errors.New("query and data_model_id cannot be empty")
}
// Create the default request payload with the provided query
requestData := SearchRequest{
Query: query,
SimilarityTopK: 9, // default value
Ref: "", // default value
NumArticlesRef: 0, // default value
}
// Apply all optional parameters to the request data
for _, opt := range opts {
opt(&requestData)
}
// Marshal the request data into JSON
reqBody, err := json.Marshal(requestData)
if err != nil {
return nil, fmt.Errorf("failed to marshal request data: %w", err)
}
// Construct the request URL with the provided data model ID
url := fmt.Sprintf("%s?data_model_id=%s", SearchBaseUrl, dataModelID)
// Create a new HTTP request
req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create new request: %w", err)
}
// Set necessary headers
req.Header.Set("Content-Type", ContentType)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", d.APIKey))
// Use the client's HTTP client to make the request
resp, err := d.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making HTTP request: %w", err)
}
defer resp.Body.Close()
// Check for non-OK status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-OK response status: %d", resp.StatusCode)
}
// Read the response body using io.ReadAll
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Unmarshal the response body into the SearchResponse structure
var result SearchResponse
err = json.Unmarshal(body, &result)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
// Check if results are available
if len(result.Response.Results) == 0 {
return nil, errors.New("no search results found")
}
// Return the result
return &result, nil
}