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
72 changes: 72 additions & 0 deletions example/src/github_latest_issues_email.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'dotenv/config'

import { httpTool } from '@fabrice-ai/tools/http'
import { createEmailTool } from '@fabrice-ai/tools/resend'
import { getApiKey } from '@fabrice-ai/tools/utils'
import { agent } from 'fabrice-ai/agent'
import { logger } from 'fabrice-ai/telemetry'
import { workflow } from 'fabrice-ai/workflow'

import { askUser } from './tools/askUser.js'

const apiKey = await getApiKey('Resend.com API Key', 'RESEND_API_KEY')
const fromEmail = await getApiKey(
'Resend.com "From" email compliant with API Key domain',
'RESEND_FROM_EMAIL'
) // must be compliant with API Key settings

const human = agent({
description: `
Use askUser tool to get the required input information for other agents`,
tools: {
askUser,
},
})

const browser = agent({
description: `
You are skilled at browsing Web with specified URLs,
methods, params etc.
You are using "httpTool" to get the data from the API and/or Web pages.
`,
tools: {
httpTool,
},
})

const wrapupRedactor = agent({
description: `
Your role is to check Github project details and check for latest issues.
`,
})

const emailSender = agent({
description: `
Your role is to send the report over email to address provided by the human`,
tools: {
...createEmailTool({
apiKey,
}),
},
})

export const checkupGithubProject = workflow({
team: { human, browser, wrapupRedactor, emailSender },
description: `
Ask human for the Github project locator: "<organization>/<project-handle>".
Browse the following URL: "https://api.github.com/repos/<organization>/<project-handle>".

From the data received get the number of stars and the URL for the listing the issues.
List last top 3 issues and the number of star gazers for the project.

Ask user for the "To" e-mail address.
Use the "${fromEmail}" as "From" e-mail address.
Create a simple HTML report and send it over to the email provided.
`,
output: `
E-mail delivered HTML report about the specified project:
- Include top 3 new issues.
- Include the actual number of star gazers.
`,
snapshot: logger,
})
76 changes: 76 additions & 0 deletions example/src/github_latest_issues_email.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'dotenv/config'

import { suite, test } from '@fabrice-ai/bdd/suite'
import { testwork } from '@fabrice-ai/bdd/testwork'
import { tool } from 'fabrice-ai/tool'
import { z } from 'zod'

import { checkupGithubProject } from './github_latest_issues_email.config.js'

export const sendEmailMock = tool({
description: 'Sends an email using the Resend.com API. Provide "from", "to", "subject", etc.',
parameters: z.object({
from: z.string().describe('The "From" address, e.g. "Acme <[email protected]>".'),
to: z
.array(z.string())
.describe('The list of recipient email addresses, e.g. ["[email protected]"].'),
subject: z.string().describe('The subject of the email.'),
text: z.string().describe('Plaintext body content of the email.'),
html: z.string().describe('HTML body content of the email.'),
}),
execute: async ({ from, to, subject, text, html }) => {
console.log(`😳 Send e-mail mock called with: \n
From: ${from}\n
To: ${to}\n
Subject: ${subject}\n
Text: ${text}
Html: ${html}
`)

return 'Email Sent!'
},
})

export const askUserMock = tool({
description: 'Tool for asking user a question',
parameters: z.object({
query: z.string().describe('The question to ask the user'),
}),
execute: async ({ query }, { provider }): Promise<string> => {
let response = '[email protected]'
if (query.toLowerCase().indexOf('github') > 0) response = 'callstackincubator/fabrice-ai'
console.log(`😳 Mocked response: ${response}\n`)
return Promise.resolve(response)
},
})

checkupGithubProject.team['human'].tools = {
askUser: askUserMock,
}

checkupGithubProject.team['emailSender'].tools = {
sendEmail: sendEmailMock,
}

const testResults = await testwork(
checkupGithubProject,
suite({
description: 'Black box testing suite',
team: {},
workflow: [
test('0_ask_for_github_handle', 'Should use the tool to ask user for github handle'),
test('1_ask_for_email', 'Should ask user for the From and To e-mails'),
test('2_browse_github', 'Should get the project and the issues list from Github'),
test('3_create_html_report', 'Should create a HTML report about the project'),
test('4_send_report', 'Should send the report to e-mail address provided'),
],
})
)

if (!testResults.passed) {
console.log('🚨 Test suite failed')
process.exit(-1)
} else {
console.log('✅ Test suite passed')
process.exit(0)
}
10 changes: 10 additions & 0 deletions example/src/github_latest_issues_email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'dotenv/config'

import { solution } from 'fabrice-ai/solution'
import { teamwork } from 'fabrice-ai/teamwork'

import { checkupGithubProject } from './github_latest_issues_email.config.js'

const result = await teamwork(checkupGithubProject)

console.log(solution(result))
87 changes: 87 additions & 0 deletions packages/tools/src/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import axios from 'axios'
import { tool } from 'fabrice-ai/tool'
import { z } from 'zod'

/**
* Configuration options for creating the Email Tool.
*/
interface EmailToolOptions {
/**
* Your Resend.com API key.
* (Required)
*/
apiKey: string

/**
* Optional override of the default Resend API endpoint.
* @default 'https://api.resend.com/emails'
*/
url?: string
}

/**
* Default values for the Email Tool options.
*/
const defaults = {
url: 'https://api.resend.com/emails',
}

/**
* Factory function to create a "sendEmail" tool for sending messages via Resend.
*/
export const createEmailTool = (options: EmailToolOptions) => {
const config = {
...defaults,
...options,
} satisfies Required<EmailToolOptions>

// Setup any default headers (authorization, etc.)
const requestConfig = {
headers: {
Authorization: `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
},
}

// Create and return the tool(s) we want to expose.
return {
sendEmail: tool({
description: 'Sends an email using the Resend.com API. Provide "from", "to", "subject", etc.',
parameters: z.object({
from: z.string().describe('The "From" address, e.g. "Acme <[email protected]>".'),
to: z
.array(z.string())
.describe('The list of recipient email addresses, e.g. ["[email protected]"].'),
subject: z.string().describe('The subject of the email.'),
text: z.string().describe('Plaintext body content of the email.'),
html: z.string().describe('HTML body content of the email.'),
}),
execute: async ({ from, to, subject, text, html }) => {
try {
const response = await axios.post(
config.url,
{
from,
to,
subject,
text,
html,
},
requestConfig
)
return typeof response.data === 'object'
? JSON.stringify(response.data)
: (response.data as string)
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(
`HTTP error ${error.response?.status}: ${error.response?.data || error.message}`
)
} else {
throw new Error(`Unknown error: ${error}`)
}
}
},
}),
}
}