mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
intitial
This commit is contained in:
@@ -232,6 +232,7 @@ libsql = ["dep:libsql"]
|
||||
integration = []
|
||||
html-to-markdown = ["dep:html-to-markdown-rs", "dep:readabilityrs"]
|
||||
bedrock = ["dep:aws-config", "dep:aws-sdk-bedrockruntime", "dep:aws-smithy-types"]
|
||||
demo = []
|
||||
import = ["dep:json5", "libsql"]
|
||||
|
||||
[[test]]
|
||||
|
||||
+5
-1
@@ -58,7 +58,9 @@ COPY channels-src/ channels-src/
|
||||
COPY wit/ wit/
|
||||
COPY providers.json providers.json
|
||||
|
||||
RUN cargo build --release --bin ironclaw
|
||||
COPY skills/ skills/
|
||||
|
||||
RUN cargo build --release --features demo --bin ironclaw
|
||||
|
||||
# Stage 5: Runtime
|
||||
FROM debian:bookworm-slim
|
||||
@@ -70,6 +72,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
COPY --from=builder /app/target/release/ironclaw /usr/local/bin/ironclaw
|
||||
COPY --from=builder /app/migrations /app/migrations
|
||||
COPY --from=builder /app/skills /app/skills
|
||||
|
||||
# Non-root user
|
||||
RUN useradd -m -u 1000 -s /bin/bash ironclaw
|
||||
@@ -78,5 +81,6 @@ USER ironclaw
|
||||
EXPOSE 3000
|
||||
|
||||
ENV RUST_LOG=ironclaw=info
|
||||
ENV SKILLS_DIR=/app/skills
|
||||
|
||||
ENTRYPOINT ["ironclaw"]
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
name: abound-remittance
|
||||
version: 0.1.0
|
||||
description: Smart remittance assistant for Abound — helps users send money to India with intelligent forex timing and transfer management.
|
||||
activation:
|
||||
keywords:
|
||||
- send money
|
||||
- transfer
|
||||
- remittance
|
||||
- exchange rate
|
||||
- forex
|
||||
- INR
|
||||
- India
|
||||
- wire
|
||||
- schedule trade
|
||||
- trade tomorrow
|
||||
- convert currency
|
||||
- send dollars
|
||||
- rupees
|
||||
- beneficiary
|
||||
- funding source
|
||||
- payment
|
||||
- how much
|
||||
- rate today
|
||||
- best time
|
||||
- family maintenance
|
||||
patterns:
|
||||
- "send \\$?\\d+"
|
||||
- "schedule.*(trade|transfer|send|wire)"
|
||||
- "how much.*(INR|rupees|India)"
|
||||
- "best time to (send|transfer|convert)"
|
||||
- "(rate|forex).*(good|bad|high|low|today|now)"
|
||||
- "transfer.*tomorrow|tomorrow.*transfer"
|
||||
tags:
|
||||
- fintech
|
||||
- remittance
|
||||
- forex
|
||||
max_context_tokens: 2500
|
||||
---
|
||||
|
||||
# Abound Remittance Assistant
|
||||
|
||||
You are a smart remittance assistant for Abound, helping users send money from USD to INR (India) with intelligent timing advice.
|
||||
|
||||
## Available Tools
|
||||
|
||||
You have these Abound-specific tools:
|
||||
|
||||
- **abound_get_account_info** — Get the user's account: limits, recipients, funding sources, payment reasons
|
||||
- **abound_get_exchange_rate** — Get current USD/INR exchange rate (current + effective after fees)
|
||||
- **abound_get_forex_score** — Get a 0-100 forex timing score with a signal (convert_now / split_transfer / wait)
|
||||
- **abound_send_wire** — Execute a wire transfer (requires: funding_source_id, beneficiary_ref_id, amount, payment_reason_key)
|
||||
- **abound_create_notification** — Send a notification to the user's Abound app
|
||||
|
||||
You also have **routine_create** for scheduling future/recurring transfers.
|
||||
|
||||
## Workflow: "Send $X" or "Transfer money"
|
||||
|
||||
1. **Always check the rate first.** Call `abound_get_exchange_rate` to get the current rate.
|
||||
2. **Check the forex score.** Call `abound_get_forex_score` to assess timing.
|
||||
3. **Get account info.** Call `abound_get_account_info` to know the user's limits, recipients, and funding sources.
|
||||
4. **Advise based on the score:**
|
||||
- **Score >= 60 (convert_now):** Tell the user it's a good time. Show the rate, the INR equivalent of their amount, and recommend proceeding.
|
||||
- **Score 40-59 (split_transfer):** Suggest splitting — send half now at the current rate, schedule the rest for later when the rate may improve.
|
||||
- **Score < 40 (wait):** Unless the transfer is urgent, recommend waiting. Explain why (rate below average, unfavorable season).
|
||||
5. **Execute if user confirms.** Use `abound_send_wire` with the correct funding_source_id, beneficiary_ref_id, amount, and payment_reason_key from the account info.
|
||||
6. **Notify.** After a successful wire, call `abound_create_notification` with relevant metadata.
|
||||
|
||||
## Workflow: "Schedule a trade" or "Send tomorrow morning"
|
||||
|
||||
1. Gather the same info (rate, score, account).
|
||||
2. Use **routine_create** to schedule the transfer:
|
||||
- For "tomorrow morning": use cron `"0 9 * * *"` with the user's timezone, set to fire once
|
||||
- For "every week": use cron `"0 9 * * MON"` (or the user's preferred day)
|
||||
- The routine prompt should instruct the agent to check the rate and execute the wire
|
||||
3. Confirm the schedule with the user, showing when it will fire.
|
||||
|
||||
## Presentation Rules
|
||||
|
||||
- Always show amounts in **both USD and INR**: "$1,000 (~INR 85,420 at today's rate of 85.42)"
|
||||
- Show the **effective rate** (after fees), not just the market rate
|
||||
- When showing the forex score, explain it simply: "The forex timing score is 72/100 — this is a good time to send."
|
||||
- If the user's amount exceeds their limit ($5,000), tell them and suggest splitting into multiple transfers
|
||||
- Always mention the **estimated delivery time** (1-3 business days) after a wire
|
||||
|
||||
## Payment Reasons
|
||||
|
||||
When asking about the purpose, offer these options:
|
||||
- Family Maintenance
|
||||
- Gift
|
||||
- Education Support
|
||||
- Medical Support
|
||||
|
||||
If the user doesn't specify, ask which applies.
|
||||
@@ -0,0 +1,563 @@
|
||||
//! Mock Abound API tools for demo purposes.
|
||||
//!
|
||||
//! These tools simulate Abound's backend API (account info, wire transfers,
|
||||
//! exchange rates, notifications, forex scoring) with realistic mock data.
|
||||
//! They are feature-gated behind `--features demo` and will be replaced by
|
||||
//! real WASM tools once Abound's backend is live.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{Datelike, Utc};
|
||||
use rand::Rng;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::context::JobContext;
|
||||
use crate::tools::tool::{ApprovalRequirement, Tool, ToolError, ToolOutput, require_str};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool 1: Get Account Info
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Returns mock Abound account data (limits, recipients, funding sources).
|
||||
pub struct AboundGetAccountInfoTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for AboundGetAccountInfoTool {
|
||||
fn name(&self) -> &str {
|
||||
"abound_get_account_info"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Retrieve the authenticated user's Abound account information including \
|
||||
transfer limits, payment reasons, recipients, and funding sources."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let data = json!({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"user_id": "acc_123456",
|
||||
"user_name": "John Doe",
|
||||
"limits": {
|
||||
"ach_limit": {
|
||||
"limit": 5000,
|
||||
"formatted_limit": "$5,000"
|
||||
}
|
||||
},
|
||||
"payment_reasons": [
|
||||
{ "key": "FAMILY_MAINTENANCE", "value": "Family Maintenance" },
|
||||
{ "key": "GIFT", "value": "Gift" },
|
||||
{ "key": "EDUCATION_SUPPORT", "value": "Education Support" },
|
||||
{ "key": "MEDICAL_SUPPORT", "value": "Medical Support" }
|
||||
],
|
||||
"recipients": [
|
||||
{
|
||||
"beneficiary_ref_id": "ben_001",
|
||||
"name": "Rahul Sharma",
|
||||
"mask": "****2222"
|
||||
}
|
||||
],
|
||||
"funding_sources": [
|
||||
{
|
||||
"funding_source_id": "fs_001",
|
||||
"bank_name": "HDFC Bank",
|
||||
"mask": "****2222"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(data, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool 2: Get Exchange Rate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Returns mock USD/INR exchange rate with slight randomization.
|
||||
pub struct AboundGetExchangeRateTool;
|
||||
|
||||
/// Generate a mock USD/INR rate with slight jitter around 85.42.
|
||||
fn mock_exchange_rate() -> f64 {
|
||||
let mut rng = rand::thread_rng();
|
||||
let jitter: f64 = rng.gen_range(-0.30..=0.30);
|
||||
((85.42 + jitter) * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for AboundGetExchangeRateTool {
|
||||
fn name(&self) -> &str {
|
||||
"abound_get_exchange_rate"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Get the current USD to INR exchange rate including the effective rate \
|
||||
after fees. Use this before initiating any wire transfer."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let rate = mock_exchange_rate();
|
||||
let effective = ((rate - 0.32) * 100.0).round() / 100.0;
|
||||
|
||||
let data = json!({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"from_currency": "USD",
|
||||
"to_currency": "INR",
|
||||
"current_exchange_rate": {
|
||||
"formatted_value": format!("{rate:.2}"),
|
||||
"value": rate
|
||||
},
|
||||
"effective_exchange_rate": {
|
||||
"formatted_value": format!("{effective:.2}"),
|
||||
"value": effective
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(data, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool 3: Send Wire
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Simulates a wire transfer. Requires approval before execution.
|
||||
pub struct AboundSendWireTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for AboundSendWireTool {
|
||||
fn name(&self) -> &str {
|
||||
"abound_send_wire"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Submit a wire transfer to send USD to an INR recipient. Requires a \
|
||||
funding source, beneficiary, amount in USD, and payment reason. \
|
||||
The transfer amount must not exceed the user's ACH limit."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"funding_source_id": {
|
||||
"type": "string",
|
||||
"description": "Funding source ID (e.g. 'fs_001')"
|
||||
},
|
||||
"beneficiary_ref_id": {
|
||||
"type": "string",
|
||||
"description": "Beneficiary reference ID (e.g. 'ben_001')"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"description": "Amount in USD to send"
|
||||
},
|
||||
"payment_reason_key": {
|
||||
"type": "string",
|
||||
"description": "Payment reason key: FAMILY_MAINTENANCE, GIFT, EDUCATION_SUPPORT, or MEDICAL_SUPPORT"
|
||||
}
|
||||
},
|
||||
"required": ["funding_source_id", "beneficiary_ref_id", "amount", "payment_reason_key"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let _funding_source = require_str(¶ms, "funding_source_id")?;
|
||||
let _beneficiary = require_str(¶ms, "beneficiary_ref_id")?;
|
||||
let _reason = require_str(¶ms, "payment_reason_key")?;
|
||||
|
||||
let amount = params
|
||||
.get("amount")
|
||||
.and_then(|v| v.as_f64())
|
||||
.ok_or_else(|| {
|
||||
ToolError::InvalidParameters("missing or invalid 'amount' parameter".to_string())
|
||||
})?;
|
||||
|
||||
// Enforce mock ACH limit
|
||||
if amount > 5000.0 {
|
||||
let data = json!({
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": "TRANSFER_NOT_ALLOWED",
|
||||
"message": format!(
|
||||
"Transfer amount ${:.2} exceeds your ACH limit of $5,000.00",
|
||||
amount
|
||||
)
|
||||
}
|
||||
});
|
||||
return Ok(ToolOutput::success(data, start.elapsed()));
|
||||
}
|
||||
|
||||
if amount <= 0.0 {
|
||||
return Err(ToolError::InvalidParameters(
|
||||
"amount must be greater than zero".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let txn_id = uuid::Uuid::new_v4();
|
||||
let trk_id = uuid::Uuid::new_v4();
|
||||
|
||||
let data = json!({
|
||||
"status": "success",
|
||||
"data": {
|
||||
"transaction_id": format!("txn_{}", &txn_id.to_string()[..8]),
|
||||
"tracking_id": format!("trk_{}", &trk_id.to_string()[..8]),
|
||||
"amount_usd": amount,
|
||||
"completion_time": {
|
||||
"min_calendar_days": 1,
|
||||
"min_business_days": 1,
|
||||
"max_calendar_days": 3,
|
||||
"max_business_days": 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(data, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_approval(&self, _params: &serde_json::Value) -> ApprovalRequirement {
|
||||
ApprovalRequirement::UnlessAutoApproved
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool 4: Create Notification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Simulates sending a notification to the Abound system.
|
||||
pub struct AboundCreateNotificationTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for AboundCreateNotificationTool {
|
||||
fn name(&self) -> &str {
|
||||
"abound_create_notification"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Create a notification in the Abound app (e.g. rate alert, transfer \
|
||||
confirmation, forex scoring signal). Returns 202 accepted."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message_id": {
|
||||
"type": "string",
|
||||
"description": "Unique message identifier"
|
||||
},
|
||||
"action_type": {
|
||||
"type": "string",
|
||||
"description": "Notification type: 'notification' or 'token_refresh'"
|
||||
},
|
||||
"meta_data": {
|
||||
"type": "object",
|
||||
"description": "Additional metadata (e.g. score, rate, signal)"
|
||||
}
|
||||
},
|
||||
"required": ["message_id", "action_type"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let message_id = require_str(¶ms, "message_id")?;
|
||||
|
||||
let data = json!({
|
||||
"status": "accepted",
|
||||
"message": "Notification request accepted for processing",
|
||||
"data": {
|
||||
"message_id": message_id
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(data, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool 5: Forex Score
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Seasonal bias factors by month (1-indexed: Jan=1 .. Dec=12).
|
||||
/// High bias in Oct-Mar (favorable remittance window), low Apr-Sep.
|
||||
const SEASONAL_BIAS: [f64; 13] = [
|
||||
0.0, // placeholder for 0-index
|
||||
0.75, // Jan
|
||||
0.70, // Feb
|
||||
0.65, // Mar
|
||||
0.35, // Apr
|
||||
0.30, // May
|
||||
0.25, // Jun
|
||||
0.25, // Jul
|
||||
0.30, // Aug
|
||||
0.35, // Sep
|
||||
0.70, // Oct
|
||||
0.75, // Nov
|
||||
0.65, // Dec
|
||||
];
|
||||
|
||||
/// Computes a forex timing score for USD/INR, biased toward interesting
|
||||
/// signals (60-80 range) for demo purposes.
|
||||
pub struct AboundGetForexScoreTool;
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for AboundGetForexScoreTool {
|
||||
fn name(&self) -> &str {
|
||||
"abound_get_forex_score"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Compute a forex timing score (0-100) for USD/INR transfers. Returns \
|
||||
a score with a signal: 'convert_now' (>=60, good time to send), \
|
||||
'split_transfer' (40-59, send half now), or 'wait' (<40, hold off). \
|
||||
Use this to advise users on optimal transfer timing."
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_params: serde_json::Value,
|
||||
_ctx: &JobContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Mock current rate and MA50
|
||||
let rate = mock_exchange_rate();
|
||||
let ma50_jitter: f64 = rng.gen_range(-0.20..=0.20);
|
||||
let ma50 = ((84.50 + ma50_jitter) * 100.0).round() / 100.0;
|
||||
|
||||
// Get seasonal bias for current month
|
||||
let month = Utc::now().month() as usize;
|
||||
let month_bias = SEASONAL_BIAS[month.clamp(1, 12)];
|
||||
|
||||
// Scoring weights
|
||||
let w_ma = 0.7;
|
||||
let w_s = 0.3;
|
||||
|
||||
// MA-based signal: how far current rate is above the 50-day average
|
||||
let ma_signal = (50.0 + ((rate - ma50) / ma50 * 100.0) * 15.0).clamp(0.0, 100.0);
|
||||
|
||||
// Combined score
|
||||
let raw_score = (ma_signal * w_ma + month_bias * 100.0 * w_s) / (w_ma + w_s);
|
||||
|
||||
// Bias toward 60-80 for demo (blend raw score with a favorable base)
|
||||
let demo_base = 68.0;
|
||||
let score = ((raw_score * 0.4 + demo_base * 0.6) as u32).clamp(55, 85);
|
||||
|
||||
let signal = if score >= 60 {
|
||||
"convert_now"
|
||||
} else if score >= 40 {
|
||||
"split_transfer"
|
||||
} else {
|
||||
"wait"
|
||||
};
|
||||
|
||||
let explanation = match signal {
|
||||
"convert_now" => format!(
|
||||
"The current USD/INR rate of {rate:.2} is above the 50-day moving average \
|
||||
of {ma50:.2}, and seasonal trends are favorable. This is a good time to \
|
||||
convert and send money."
|
||||
),
|
||||
"split_transfer" => format!(
|
||||
"The rate of {rate:.2} is near the 50-day average of {ma50:.2}. Consider \
|
||||
splitting your transfer \u{2014} send half now and hold the rest for a \
|
||||
potentially better rate."
|
||||
),
|
||||
_ => format!(
|
||||
"The current rate of {rate:.2} is below the 50-day average of {ma50:.2}. \
|
||||
Unless urgent, consider waiting for a better rate."
|
||||
),
|
||||
};
|
||||
|
||||
let data = json!({
|
||||
"score": score,
|
||||
"signal": signal,
|
||||
"rate": rate,
|
||||
"ma50": ma50,
|
||||
"month_bias": month_bias,
|
||||
"explanation": explanation
|
||||
});
|
||||
|
||||
Ok(ToolOutput::success(data, start.elapsed()))
|
||||
}
|
||||
|
||||
fn requires_sanitization(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::context::JobContext;
|
||||
|
||||
fn test_ctx() -> JobContext {
|
||||
JobContext::with_user("test-user", "test", "mock abound tool test")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_info_returns_valid_json() {
|
||||
let tool = AboundGetAccountInfoTool;
|
||||
let result = tool.execute(json!({}), &test_ctx()).await.unwrap();
|
||||
let data = &result.result["data"];
|
||||
assert_eq!(data["user_id"], "acc_123456");
|
||||
assert_eq!(data["limits"]["ach_limit"]["limit"], 5000);
|
||||
assert_eq!(data["recipients"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(data["funding_sources"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exchange_rate_returns_valid_range() {
|
||||
let tool = AboundGetExchangeRateTool;
|
||||
let result = tool.execute(json!({}), &test_ctx()).await.unwrap();
|
||||
let rate = result.result["data"]["current_exchange_rate"]["value"]
|
||||
.as_f64()
|
||||
.unwrap();
|
||||
// Rate should be within jitter range of base 85.42
|
||||
assert!(rate > 85.0 && rate < 85.8, "rate {rate} out of range");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_wire_succeeds_within_limit() {
|
||||
let tool = AboundSendWireTool;
|
||||
let params = json!({
|
||||
"funding_source_id": "fs_001",
|
||||
"beneficiary_ref_id": "ben_001",
|
||||
"amount": 1000.0,
|
||||
"payment_reason_key": "FAMILY_MAINTENANCE"
|
||||
});
|
||||
let result = tool.execute(params, &test_ctx()).await.unwrap();
|
||||
assert_eq!(result.result["status"], "success");
|
||||
assert!(result.result["data"]["transaction_id"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("txn_"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_wire_rejects_over_limit() {
|
||||
let tool = AboundSendWireTool;
|
||||
let params = json!({
|
||||
"funding_source_id": "fs_001",
|
||||
"beneficiary_ref_id": "ben_001",
|
||||
"amount": 6000.0,
|
||||
"payment_reason_key": "FAMILY_MAINTENANCE"
|
||||
});
|
||||
let result = tool.execute(params, &test_ctx()).await.unwrap();
|
||||
assert_eq!(result.result["status"], "error");
|
||||
assert_eq!(result.result["error"]["code"], "TRANSFER_NOT_ALLOWED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_wire_requires_approval() {
|
||||
let tool = AboundSendWireTool;
|
||||
assert!(matches!(
|
||||
tool.requires_approval(&json!({})),
|
||||
ApprovalRequirement::UnlessAutoApproved
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_notification_returns_accepted() {
|
||||
let tool = AboundCreateNotificationTool;
|
||||
let params = json!({
|
||||
"message_id": "msg_001",
|
||||
"action_type": "notification",
|
||||
"meta_data": { "score": 72 }
|
||||
});
|
||||
let result = tool.execute(params, &test_ctx()).await.unwrap();
|
||||
assert_eq!(result.result["status"], "accepted");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn forex_score_in_demo_range() {
|
||||
let tool = AboundGetForexScoreTool;
|
||||
// Run multiple times to check range stability
|
||||
for _ in 0..20 {
|
||||
let result = tool.execute(json!({}), &test_ctx()).await.unwrap();
|
||||
let score = result.result["score"].as_u64().unwrap();
|
||||
assert!(
|
||||
(55..=85).contains(&score),
|
||||
"score {score} outside demo range [55, 85]"
|
||||
);
|
||||
let signal = result.result["signal"].as_str().unwrap();
|
||||
assert!(
|
||||
signal == "convert_now" || signal == "split_transfer" || signal == "wait",
|
||||
"unexpected signal: {signal}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Built-in tools that come with the agent.
|
||||
|
||||
#[cfg(feature = "demo")]
|
||||
mod abound;
|
||||
mod echo;
|
||||
pub mod extension_tools;
|
||||
mod file;
|
||||
@@ -17,6 +19,11 @@ pub mod skill_tools;
|
||||
mod time;
|
||||
mod tool_info;
|
||||
|
||||
#[cfg(feature = "demo")]
|
||||
pub use abound::{
|
||||
AboundCreateNotificationTool, AboundGetAccountInfoTool, AboundGetExchangeRateTool,
|
||||
AboundGetForexScoreTool, AboundSendWireTool,
|
||||
};
|
||||
pub use echo::EchoTool;
|
||||
pub use extension_tools::{
|
||||
ExtensionInfoTool, ToolActivateTool, ToolAuthTool, ToolInstallTool, ToolListTool,
|
||||
|
||||
@@ -16,6 +16,11 @@ use crate::skills::registry::SkillRegistry;
|
||||
use crate::tools::builder::{
|
||||
BuildSoftwareTool, BuilderConfig, LlmSoftwareBuilder, SoftwareBuilder,
|
||||
};
|
||||
#[cfg(feature = "demo")]
|
||||
use crate::tools::builtin::{
|
||||
AboundCreateNotificationTool, AboundGetAccountInfoTool, AboundGetExchangeRateTool,
|
||||
AboundGetForexScoreTool, AboundSendWireTool,
|
||||
};
|
||||
use crate::tools::builtin::{
|
||||
ApplyPatchTool, CancelJobTool, CreateJobTool, EchoTool, ExtensionInfoTool, HttpTool,
|
||||
JobEventsTool, JobPromptTool, JobStatusTool, JsonTool, ListDirTool, ListJobsTool,
|
||||
@@ -248,6 +253,16 @@ impl ToolRegistry {
|
||||
}
|
||||
self.register_sync(Arc::new(http));
|
||||
|
||||
// Abound demo tools (mock APIs — only compiled with --features demo)
|
||||
#[cfg(feature = "demo")]
|
||||
{
|
||||
self.register_sync(Arc::new(AboundGetAccountInfoTool));
|
||||
self.register_sync(Arc::new(AboundGetExchangeRateTool));
|
||||
self.register_sync(Arc::new(AboundSendWireTool));
|
||||
self.register_sync(Arc::new(AboundCreateNotificationTool));
|
||||
self.register_sync(Arc::new(AboundGetForexScoreTool));
|
||||
}
|
||||
|
||||
tracing::debug!("Registered {} built-in tools", self.count());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user