Skip to content

Commit

Permalink
feat: add GoogleSearchTool
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jan 23, 2025
1 parent 66087a6 commit 52c99b7
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 42 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dotenv = "0.15"
schemars = { version = "0.8" }
clap = { version = "4.5", features = ["derive", "env"] }
idna = "1.0" # https://github.com/ldclabs/anda/security/dependabot/1
url = "2.5"

[workspace.metadata.cargo-shear]
ignored = ["idna"]
Expand Down
21 changes: 0 additions & 21 deletions LICENSE

This file was deleted.

8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ anda/
│ └── .../ # More agents in future releases
├── tools/ # Tool libraries
│ ├── anda_icp/ # Anda agent tools offers integration with the Internet Computer (ICP).
│ └── .../ # More agents in future releases
│ └── .../ # More tools in future releases
└── characters/ # characters examples
```

### How to Use and Contribute

#### For Non-Developers:

You can follow the agents in the `agents` directory. For example, [`anda_bot`](https://github.com/ldclabs/anda/tree/main/agents/anda_bot)—simply copy the [nitro_enclave](https://github.com/ldclabs/anda/tree/main/agents/anda_bot/nitro_enclave) folder, modify the `Character.toml` role definition and `Config.toml` runtime parameters, and deploy according to the tutorial.
You can follow the agents in the `agents` directory. For example, [`anda_bot`](https://github.com/ldclabs/anda/tree/main/agents/anda_bot)—simply copy the [nitro_enclave](https://github.com/ldclabs/anda/tree/main/agents/anda_bot/nitro_enclave) folder, modify the `Character.toml` role definition and `Config.toml` runtime parameters, and deploy according to the tutorial.
The deployment process is currently complex, but we plan to launch a cloud service for one-click deployment in the future.

#### For Developers:

- Add more integration tools with external services in `tools`;
- Create more agent applications in `agents`;
- Add more integration tools with external services in `tools`;
- Create more agent applications in `agents`;
- Or enhance the core engines `anda_core` and `anda_engine`.

### Related Projects
Expand Down
8 changes: 5 additions & 3 deletions anda_core/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
//! # Usage
//!
//! ## Reference Implementations
//! 1. [`SubmitTool`](https://github.com/ldclabs/anda/blob/main/anda_engine/src/extension/extractor.rs) -
//! 1. [`GoogleSearchTool`](https://github.com/ldclabs/anda/blob/main/anda_engine/src/extension/google.rs) -
//! A tool for performing web searches and retrieve results
//! 2. [`SubmitTool`](https://github.com/ldclabs/anda/blob/main/anda_engine/src/extension/extractor.rs) -
//! A tool for extracting structured data using LLMs
//! 2. [`TransferTool`](https://github.com/ldclabs/anda/blob/main/tools/anda_icp/src/ledger/transfer.rs) -
//! 3. [`TransferTool`](https://github.com/ldclabs/anda/blob/main/tools/anda_icp/src/ledger/transfer.rs) -
//! A tool for handling ICP blockchain transfers
//! 3. [`BalanceOfTool`](https://github.com/ldclabs/anda/blob/main/tools/anda_icp/src/ledger/balance.rs) -
//! 4. [`BalanceOfTool`](https://github.com/ldclabs/anda/blob/main/tools/anda_icp/src/ledger/balance.rs) -
//! A tool for querying ICP blockchain balances
//!
//! These reference implementations share a common feature: they automatically generate the JSON Schema
Expand Down
1 change: 1 addition & 0 deletions anda_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tokio = { workspace = true }
log = { workspace = true }
chrono = { workspace = true }
schemars = { workspace = true }
url = { workspace = true }

[dev-dependencies]
dotenv = { workspace = true }
248 changes: 248 additions & 0 deletions anda_engine/src/extension/google.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
//! Google Search Extension for Anda Engine
//!
//! This module provides integration with Google's Custom Search API, allowing
//! the engine to perform web searches and retrieve results.
//!
//! # Features
//! - Perform web searches using Google's Custom Search API
//! - Parse and return structured search results
//! - Configurable number of results
//! - Integration with Anda's HTTP features
//!
//! # Configuration
//! Requires:
//! - Google API Key
//! - Custom Search Engine ID
//!
//! # Usage
//! ```rust,ignore
//! let google = GoogleSearchTool::new(api_key, search_engine_id, Some(5));
//! // Manual invocation within an agent
//! let results = google.search(ctx, SearchArgs { query: "ICPanda" }).await?;
//! // Or register with Engine for automatic invocation
//! let engine = Engine::builder()
//! .with_name("MyEngine".to_string())
//! .register_tool(google_search)?
//! .register_agent(my_agent)?
//! .build("default_agent".to_string())?;
//! ```

use anda_core::{BoxError, FunctionDefinition, HttpFeatures, Tool};
use http::header;
use schemars::{schema_for, JsonSchema};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use url::Url;

use crate::context::BaseCtx;

/// Arguments for Google search query
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
pub struct SearchArgs {
/// The search query string
pub query: String,
}

/// Represents a single search result item
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
pub struct SearchResultItem {
/// Title of the search result
pub title: String,
/// URL of the search result
pub link: String,
/// Short description snippet of the result
pub snippet: String,
}

/// Google Search Tool implementation
///
/// Provides functionality to perform web searches using Google's Custom Search API.
///
/// # Prerequisites
/// - Enable Custom Search API at https://console.cloud.google.com/
/// - Obtain API Key and Custom Search Engine ID
///
/// # API Reference
/// - Official documentation: https://developers.google.com/custom-search/v1/using_rest
#[derive(Debug, Clone)]
pub struct GoogleSearchTool {
/// Google API key for authentication
api_key: String,
/// Custom Search Engine ID
search_engine_id: String,
/// Number of results to return
result_number: u8,
/// JSON schema for the search arguments
schema: Value,
}

impl GoogleSearchTool {
/// Creates a new GoogleSearchTool instance
///
/// # Arguments
/// * `api_key` - Google API key
/// * `search_engine_id` - Custom Search Engine ID
/// * `result_number` - Optional number of results to return (defaults to 5)
pub fn new(api_key: String, search_engine_id: String, result_number: Option<u8>) -> Self {
let mut schema = schema_for!(SearchArgs);
schema.meta_schema = None; // Remove the $schema field

GoogleSearchTool {
api_key,
search_engine_id,
result_number: result_number.unwrap_or(5),
schema: json!(schema),
}
}

/// Performs a Google search using the provided query
///
/// # Arguments
/// * `ctx` - HTTP context for making requests
/// * `args` - Search arguments containing the query
///
/// # Returns
/// Vector of search result items or an error
pub async fn search(
&self,
ctx: &impl HttpFeatures,
args: SearchArgs,
) -> Result<Vec<SearchResultItem>, BoxError> {
let mut url = Url::parse("https://www.googleapis.com/customsearch/v1")?;
let mut headers = header::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
"application/json".parse().expect("invalid header value"),
);
headers.insert(
header::ACCEPT_ENCODING,
"gzip".parse().expect("invalid header value"),
);

url.query_pairs_mut()
.append_pair("key", &self.api_key)
.append_pair("cx", &self.search_engine_id)
.append_pair("num", self.result_number.to_string().as_str())
.append_pair("q", args.query.as_str());

let response = ctx
.https_call(url.as_str(), http::Method::GET, Some(headers), None)
.await?;

if !response.status().is_success() {
return Err(format!(
"Google customsearch API returned status: {}",
response.status()
)
.into());
}

let json: Value = response.json().await?;
let mut res = Vec::new();
if let Some(items) = json.get("items").and_then(|v| v.as_array()) {
for item in items {
if let (Some(title), Some(link), Some(snippet)) = (
item.get("title").and_then(|v| v.as_str()),
item.get("link").and_then(|v| v.as_str()),
item.get("snippet").and_then(|v| v.as_str()),
) {
res.push(SearchResultItem {
title: title.to_string(),
link: link.to_string(),
snippet: snippet.to_string(),
});
}
}
}

Ok(res)
}
}

impl Tool<BaseCtx> for GoogleSearchTool {
const CONTINUE: bool = true;
type Args = SearchArgs;
type Output = Vec<SearchResultItem>;

fn name(&self) -> String {
"google_web_search".to_string()
}

fn description(&self) -> String {
"Performs a google web search for your query then returns a string of the top search results.".to_string()
}

fn definition(&self) -> FunctionDefinition {
FunctionDefinition {
name: self.name(),
description: self.description(),
parameters: self.schema.clone(),
strict: Some(true),
}
}

/// Executes the search operation
///
/// # Arguments
/// * `ctx` - Base context
/// * `args` - Search arguments
///
/// # Returns
/// Vector of search results or an error
async fn call(&self, ctx: BaseCtx, args: Self::Args) -> Result<Self::Output, BoxError> {
self.search(&ctx, args).await
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{engine::EngineBuilder, model::Model};

#[tokio::test]
#[ignore]
async fn test_google_search_tool() {
dotenv::dotenv().ok();

let api_key = std::env::var("GOOGLE_API_KEY").expect("GOOGLE_API_KEY is not set");
let search_engine_id =
std::env::var("GOOGLE_SEARCH_ENGINE_ID").expect("GOOGLE_SEARCH_ENGINE_ID is not set");
let tool = GoogleSearchTool::new(api_key, search_engine_id, Some(6));
let definition = tool.definition();
assert_eq!(tool.name(), "google_web_search");
println!("{}", serde_json::to_string_pretty(&definition).unwrap());
// {
// "name": "google_web_search",
// "description": "Performs a google web search for your query then returns a string of the top search results.",
// "parameters": {
// "description": "The search query to perform.",
// "properties": {
// "query": {
// "type": "string"
// }
// },
// "required": [
// "query"
// ],
// "title": "SearchArgs",
// "type": "object"
// },
// "strict": true
// }

let ctx = EngineBuilder::new()
.with_model(Model::mock_implemented())
.mock_ctx();
let res = tool
.search(
&ctx,
SearchArgs {
query: "ICPanda".to_string(),
},
)
.await
.unwrap();
print!("{:?}", res);
}
}
16 changes: 2 additions & 14 deletions anda_engine/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! - **Attention Management**: Controls how agents focus on and respond to content
//! - **Character System**: Defines agent personalities and communication styles
//! - **Extraction Tools**: Enables structured data extraction from unstructured text
//! - **Google Web Search Tool**: Enables web searches and retrieve results.
//! - **Document Segmentation**: Breaks down large documents into manageable chunks
//!
//! # Usage
Expand All @@ -19,22 +20,9 @@
//! 3. Use extraction tools for structured data processing
//! 4. Apply document segmentation for large content processing
//!
//! # Example
//! ```rust,ignore
//! use anda_engine::extension::{
//! Attention,
//! Character,
//! DocumentSegmenter,
//! Extractor
//! };
//!
//! // Create a basic agent configuration
//! let attention = Attention::default();
//! let character = Character::default();
//! let segmenter = DocumentSegmenter::new(500, 8000);
//! ```
pub mod attention;
pub mod character;
pub mod extractor;
pub mod google;
pub mod segmenter;

0 comments on commit 52c99b7

Please sign in to comment.