This commit is contained in:
geoffsee
2025-05-23 09:48:26 -04:00
commit 66d3c06230
84 changed files with 6529 additions and 0 deletions

4
packages/core/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from "../genaiscript/genaisrc/_state";
export * from "./news";
export * from "./quotes";
export * from "./types";

View File

@@ -0,0 +1,14 @@
import {ApiResponse} from "./types";
export async function collect_gainers_losers(x: { apiKey: string, limit: number }): Promise<ApiResponse> {
const { apiKey, limit } = x;
//
const data: ApiResponse = await fetch(`https://pro-api.coinmarketcap.com/v1/cryptocurrency/trending/gainers-losers?limit=${limit}`, {
headers: {
"x-cmc_pro_api_key": apiKey
}
}).then((symbolDataRequest) => symbolDataRequest.json());
return data;
}

View File

@@ -0,0 +1,48 @@
type Quote = {
price: number;
volume_24h: number;
percent_change_1h: number;
percent_change_24h: number;
percent_change_7d: number;
market_cap: number;
last_updated: string;
};
type Platform = null;
type Tag = string;
type Data = {
id: number;
name: string;
symbol: string;
slug: string;
cmc_rank?: number;
num_market_pairs: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
last_updated: string;
date_added: string;
tags: Tag[];
platform: Platform;
quote: {
USD: Quote;
BTC?: Quote;
ETH?: Quote;
};
};
type Status = {
timestamp: string;
error_code: number;
error_message: string | null;
elapsed: number;
credit_count: number;
};
export type ApiResponse = {
data: Data[];
status: Status;
};

178
packages/core/news/index.ts Normal file
View File

@@ -0,0 +1,178 @@
import {types, Instance} from 'mobx-state-tree';
import {runInAction} from "mobx";
const Article = types.model('Article', {
title: types.string,
content: types.string,
url: types.maybe(types.string),
source: types.maybe(types.string),
pubDate: types.maybe(types.string),
summary: types.maybe(types.string),
description: types.maybe(types.string),
authorsByline: types.maybe(types.string),
shortSummary: types.maybe(types.string),
labels: types.maybe(types.frozen()),
imageUrl: types.maybe(types.string),
score: types.maybe(types.number),
});
export const NewsStore = types
.model('NewsStore', {
symbolsNews: types.map(types.array(Article)),
isLoading: types.boolean,
error: types.maybe(types.string),
apiKey: types.string,
})
.actions((self) => ({
addNews(symbol: string, articles: any[]) {
if (!self.symbolsNews.has(symbol)) {
self.symbolsNews.set(symbol, []);
}
const mappedArticles = articles.map((article) => Article.create({
title: article.title || 'No Title',
content: article.content || 'No Content',
url: article.url,
source: article.domain,
pubDate: article.pubDate,
summary: article.summary,
description: article.description,
authorsByline: article.authorsByline,
shortSummary: article.shortSummary,
labels: article.labels,
imageUrl: article.imageUrl,
score: article.score,
}));
self.symbolsNews.get(symbol)!.push(...mappedArticles);
self.isLoading = false;
},
clearNews(symbol: string) {
if (self.symbolsNews.has(symbol)) {
self.symbolsNews.set(symbol, []);
}
},
setLoading(loading: boolean) {
self.isLoading = loading;
},
setError(message: string) {
self.error = message;
self.isLoading = false;
},
async fetchNewsForSymbol(symbol: string, limit: number, sort: "date" | "relevance") {
self.setLoading(true);
self.setError(undefined);
try {
await runInAction(async () => {
const newsData = await collect_news({symbol, apiKey: self.apiKey, limit, sort});
if (newsData && newsData.articles) {
self.addNews(symbol, newsData.articles);
} else {
self.setError("Failed to fetch news or invalid response format.");
}
})
} catch (err: any) {
console.error('Error fetching news:', err);
self.setError(err.message || "Failed to fetch news.");
}
},
}))
.views((self) => ({
getNewsForSymbol(symbol: string) {
return self.symbolsNews.get(symbol) || [];
},
getAllSymbols() {
return Array.from(self.symbolsNews.keys());
},
hasNewsForSymbol(symbol: string) {
return self.symbolsNews.has(symbol) && self.symbolsNews.get(symbol)!.length > 0;
},
}));
export type INewsStore = Instance<typeof NewsStore>;
export const createNewsStore = (apikey, perigon) => NewsStore.create({
symbolsNews: {},
isLoading: false,
error: undefined,
apiKey: apikey,
});
/* @collect_news return value structure
{
news: {
status: 200,
numResults: 4080,
articles: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object]
]
}
}
*/
export async function collect_news(x: { symbol: string, apiKey: string, limit: number, sort: "date" | "relevance" }) {
const {symbol, apiKey, limit, sort} = x;
const symbolNameMap = {
"BTC": "Bitcoin",
"ETH": "Ethereum",
"XRP": "Ripple",
"LTC": "Litecoin",
"ADA": "Cardano",
"DOGE": "Dogecoin",
"BNB": "Binance Coin",
"DOT": "Polkadot",
"SOL": "Solana",
"AVAX": "Avalanche"
};
const cryptocurrencyName = symbolNameMap[symbol] ?? symbol;
const rawContentQuery = "scandal OR \"corporate misconduct*\" OR fraud OR \"financial irregularities*\" OR lawsuit OR \"legal action*\" OR bankruptcy OR \"financial distress*\" OR \"data breach\" OR \"security vulnerability*\" OR \"environmental impact\" OR \"ecological damage*\" OR \"labor dispute\" OR \"worker rights*\" OR \"product failure\" OR \"quality issue*\" OR \"ethical concern\" OR \"moral dilemma*\" OR \"health risk\" OR \"safety hazard*\" OR \"regulatory violation\" OR \"compliance issue*\" OR \"market manipulation\" OR \"trading irregularity*\" OR \"public relations crisis\" OR \"reputation damage*\" OR \"political controversy\" OR \"government intervention*\" OR \"consumer complaint\" OR \"customer dissatisfaction*\" OR \"supply chain disruption\" OR \"logistics problem*\" OR \"intellectual property dispute\" OR \"patent infringement*\"";
const contentQuery = encodeURIComponent(rawContentQuery);
const rawTitleQuery = `${cryptocurrencyName} OR ${symbol} OR "${cryptocurrencyName} price" OR "${cryptocurrencyName} market" OR "${cryptocurrencyName} news"`;
const titleQuery = encodeURIComponent(rawTitleQuery);
try {
const result = await allNews({
q: contentQuery,
title: titleQuery,
size: limit,
sortBy: sort,
apiKey: apiKey
});
return result.data;
} catch (err) {
console.error('Error fetching news:', err);
throw err;
}
}

View File

@@ -0,0 +1,75 @@
import {describe, expect, it} from 'vitest';
import {collect_news, createNewsStore, NewsStore} from './index';
const testApiKey = '';
describe('NewsStore', () => {
it('should create a NewsStore instance', () => {
const store = createNewsStore(testApiKey);
expect(store).toBeDefined();
expect(store.isLoading).toBe(false);
expect(store.error).toBeUndefined();
expect(store.getAllSymbols()).toEqual([]);
});
it('should add news articles for a symbol', () => {
const store = createNewsStore(testApiKey);
const articles = [
{ title: 'Article 1', content: 'Content 1', url: 'http://example.com/1', source: 'Source 1', publishedAt: '2025-01-01' },
{ title: 'Article 2', content: 'Content 2', url: 'http://example.com/2', source: 'Source 2', publishedAt: '2025-01-02' }
];
store.addNews('BTC', articles);
expect(store.getNewsForSymbol('BTC')).toHaveLength(2);
expect(store.getNewsForSymbol('BTC')[0].title).toBe('Article 1');
expect(store.getNewsForSymbol('BTC')[1].title).toBe('Article 2');
expect(store.hasNewsForSymbol('BTC')).toBe(true);
});
it('should clear news articles for a symbol', () => {
const store = createNewsStore(testApiKey);
const articles = [
{ title: 'Article 1', content: 'Content 1', url: 'http://example.com/1', source: 'Source 1', publishedAt: '2025-01-01' }
];
store.addNews('BTC', articles);
store.clearNews('BTC');
expect(store.getNewsForSymbol('BTC')).toHaveLength(0);
expect(store.hasNewsForSymbol('BTC')).toBe(false);
});
it('should handle fetchNewsForSymbol successfully', async () => {
const store = createNewsStore(testApiKey);
await store.fetchNewsForSymbol('BTC', 10, 'date');
const storeNews = store.getNewsForSymbol('BTC');
console.log(storeNews);
expect(storeNews).toHaveLength(10);
expect(store.getNewsForSymbol('BTC')[0].title).toBeTypeOf("string");
expect(store.isLoading).toBe(false);
expect(store.error).toBeUndefined();
});
it('should throw an error for invalid symbol in collect_news', async () => {
await expect(collect_news({ symbol: 'INVALID', apiKey: testApiKey, limit: 10, sort: 'date' }))
.rejects.toThrow('Invalid symbol: INVALID. Must be one of BTC, ETH, XRP, LTC, ADA, DOGE, BNB, DOT, SOL, AVAX.');
});
it('should fetch news using collect_news', async () => {
const result = await collect_news({ symbol: 'BTC', apiKey: testApiKey, limit: 1, sort: 'date' });
expect(result).toBeDefined();
expect(result.status).toBe(200);
expect(result.articles).toBeDefined();
});
});

View File

@@ -0,0 +1,9 @@
{
"name": "@web-agent-rs/core",
"version": "1.0.0",
"type": "module",
"dependencies": {
"mobx-state-tree": "^7.0.2",
"@web-agent-rs/perigon": "workspace:*"
}
}

View File

@@ -0,0 +1,137 @@
import {getSnapshot, types} from "mobx-state-tree";
import {NewsStore} from "../news";
const PortfolioCashModel = types.model("PortfolioCash", {
amount: types.number,
currency: types.enumeration("Currency", ["USD", "BTC"]),
});
const PortfolioActionModel = types.model("PortfolioAction", {
action: types.enumeration("Action", ["buy", "sell", "hold"]),
symbol: types.string,
quantity: types.number,
timestamp: types.Date,
});
export const PortfolioNewsportfolioNewsModel = types.model("PortfolioNews", {
symbol: types.string,
date_created: types.string,
news: types.array(types.model("NewsItem", {
symbol: types.maybe(types.string),
date_created: types.maybe(types.string),
news: types.string,
timestamp: types.maybe(types.string),
})),
timestamp: types.maybe(types.string),
});
export const portfolioQuoteModel = types.model("PortfolioQuote", {
symbol: types.string,
quote: types.string,
date_created: types.string,
});
const PortfolioAssetContextModel = types.model("PortfolioAssetContext", {
timestamp: types.Date,
portfolio_snapshot: PortfolioCashModel,
});
const PortfolioAssetModel = types.model("PortfolioAsset", {
symbol: types.string,
quantity: types.number,
recommended_action: types.maybe(
types.enumeration("RecommendedAction", ["buy", "sell", "hold"])
),
last_taken_action: types.optional(types.enumeration("LastAction", ["buy", "sell", "hold", "none", "never"]), "never"),
context: PortfolioAssetContextModel,
});
const PortfolioModel = types
.model("Portfolio", {
supportedSymbols: types.array(types.string),
liquidity: PortfolioCashModel,
actions: types.optional(types.array(PortfolioActionModel), []),
assets: types.array(PortfolioAssetModel),
news: types.optional(types.array(NewsStore), []),
quotes: types.optional(types.array(portfolioQuoteModel), []),
})
.actions((self) => ({
addAction(actionData: {
action: "buy" | "sell" | "hold";
symbol: string;
quantity: number;
}) {
self.actions.push({
...actionData,
timestamp: new Date(),
});
},
addNews(newsData: any) {
self.news.push({
...newsData,
timestamp: new Date(),
});
},
addQuote(quoteData: any) {
self.quotes
self.quotes.push({
...quoteData,
timestamp: new Date(),
});
},
updateLiquidity(amount: number) {
self.liquidity.amount = amount;
},
}));
const tokenList = [
"AAVE", "AVAX", "BAT", "BCH", "BTC",
"CRV", "DOGE", "DOT", "ETH", "GRT",
"LINK", "LTC", "MKR", "SHIB", "SUSHI",
"UNI", "USDC", "USDT", "XTZ", "YFI",
];
const portfolioCash = PortfolioCashModel.create({
amount: 10000,
currency: "USD",
});
const portfolioAssets = tokenList.map((token) =>
PortfolioAssetModel.create({
symbol: token,
quantity: 0,
recommended_action: "hold",
last_taken_action: undefined,
context: {
timestamp: new Date(),
portfolio_snapshot: getSnapshot(portfolioCash),
},
})
);
const portfolioActions = [];
const portfolioNews = [];
const portfolioQuotes = [];
const portfolioInstance = PortfolioModel.create({
liquidity: portfolioCash,
actions: portfolioActions,
assets: portfolioAssets,
supportedSymbols: tokenList,
news: portfolioNews,
quotes: portfolioQuotes
});
export default portfolioInstance;

View File

@@ -0,0 +1,13 @@
import {ApiResponse} from "./types";
export async function collect_quote(x: { symbol: string, apiKey: string }) {
const {symbol, apiKey} = x;
const data: ApiResponse = await fetch(`https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest?symbol=${symbol}`, {
headers: {
"x-cmc_pro_api_key": apiKey
}
}).then((symbolDataRequest) => symbolDataRequest.json());
return data;
}

View File

@@ -0,0 +1,77 @@
import {types, flow, Instance} from "mobx-state-tree";
import {collect_quote} from './index';
const QuoteData = types.optional(types.frozen(), {})
export const QuoteStore = types
.model("QuoteStore", {
apiKey: types.string,
quotes: types.map(QuoteData),
})
.views(self => ({
getQuote(symbol) {
return self.quotes.get(symbol);
},
hasQuote(symbol) {
return self.quotes.has(symbol);
}
}))
.actions(self => {
const extractUsefulData = (data, symbol) => {
return data.data[symbol].map(qd => ({
symbol: qd.symbol,
slug: qd.slug,
tags: qd.tags,
id: qd.id,
...qd.quote.USD
})).at(0);
};
const fetchQuote = flow(function* (symbol) {
try {
const data = yield collect_quote({symbol, apiKey: self.apiKey});
const usefulData = extractUsefulData(data, symbol);
self.quotes.set(symbol, usefulData);
return usefulData;
} catch (error) {
console.error(`An error occurred fetching the quote for symbol: ${symbol}`, error);
throw error;
}
});
const fetchQuotes = flow(function* (symbols) {
const results = {};
for (const symbol of symbols) {
if (self.quotes.has(symbol)) {
results[symbol] = self.quotes.get(symbol);
} else {
const data = yield fetchQuote(symbol);
results[symbol] = extractUsefulData(data, symbol);
}
}
return results;
});
const clearCache = () => {
self.quotes.clear();
};
return {
fetchQuote,
fetchQuotes,
clearCache
};
});
export type QuoteManagerType = Instance<typeof QuoteStore>;

View File

@@ -0,0 +1,17 @@
import {describe, it} from 'vitest';
import {QuoteStore} from "./models";
describe('QuoteStore', () => {
it('should get data for symbols using the quoteManager', async () => {
const testApiKey = '';
const quoteManager = QuoteStore.create({
apiKey: testApiKey,
});
const symbol = 'BTC';
const data = await quoteManager.fetchQuote(symbol);
console.log(JSON.stringify(data));
});
});

View File

@@ -0,0 +1,71 @@
type Status = {
timestamp: string;
error_code: number;
error_message: string | null;
elapsed: number;
credit_count: number;
notice: string | null;
};
type Tag = {
slug: string;
name: string;
category: string;
};
type Quote = {
USD: {
price: number | null;
volume_24h: number;
volume_change_24h: number;
percent_change_1h: number;
percent_change_24h: number;
percent_change_7d: number;
percent_change_30d: number;
percent_change_60d: number;
percent_change_90d: number;
market_cap: number | null;
market_cap_dominance: number | null;
fully_diluted_market_cap: number | null;
tvl: number | null;
last_updated: string;
};
};
type Platform = {
id: number;
name: string;
symbol: string;
slug: string;
token_address: string;
};
type Cryptocurrency = {
id: number;
name: string;
symbol: string;
slug: string;
num_market_pairs: number;
date_added: string;
tags: Tag[];
max_supply: number | null;
circulating_supply: number | null;
total_supply: number;
platform: Platform | null;
is_active: number;
infinite_supply: boolean;
cmc_rank: number | null;
is_fiat: number;
self_reported_circulating_supply: number | null;
self_reported_market_cap: number | null;
tvl_ratio: number | null;
last_updated: string;
quote: Quote;
};
export type ApiResponse = {
status: Status;
data: {
[SYMBOL: string]: Cryptocurrency[];
};
};

View File

@@ -0,0 +1,69 @@
export type CryptoDataResponse = {
status: {
timestamp: string;
error_code: number;
error_message: string | null;
elapsed: number;
credit_count: number;
notice: string | null;
};
data: {
[key: string]: CryptoAsset[];
};
};
export type CryptoAsset = {
id: number;
name: string;
symbol: string;
slug: string;
num_market_pairs: number;
date_added: string;
tags: CryptoTag[];
max_supply: number | null;
circulating_supply: number;
total_supply: number;
platform: CryptoPlatform | null;
is_active: number;
infinite_supply: boolean;
cmc_rank: number;
is_fiat: number;
self_reported_circulating_supply: number | null;
self_reported_market_cap: number | null;
tvl_ratio: number;
last_updated: string;
quote: {
[currency: string]: CryptoQuote;
};
};
export type CryptoTag = {
slug: string;
name: string;
category: string;
};
export type CryptoPlatform = {
id: number;
name: string;
symbol: string;
slug: string;
token_address: string;
};
export type CryptoQuote = {
price: number;
volume_24h: number;
volume_change_24h: number;
percent_change_1h: number;
percent_change_24h: number;
percent_change_7d: number;
percent_change_30d: number;
percent_change_60d: number;
percent_change_90d: number;
market_cap: number;
market_cap_dominance: number;
fully_diluted_market_cap: number;
tvl: number;
last_updated: string;
};

View File

@@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -0,0 +1,15 @@
# genaiscript-rust-shim
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.1.36. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

Binary file not shown.

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env node
import minimist from "minimist";
import { run } from "genaiscript/api";
import { RunScriptOptions } from "./shim-types";
type Args = {
file: string;
vars: Record<string, unknown>;
options: Partial<RunScriptOptions> & {
envVars?: Record<string, string>;
signal?: AbortSignal;
};
};
async function wrapper(args: Args) {
try {
await run(args.file, [], { vars: args.vars }, args.options);
} catch (error) {
console.error("Error executing script:", error);
process.exit(1);
}
}
function parseCliArgs(): Args {
const argv = minimist(process.argv.slice(2), {
string: ["file"],
alias: { f: "file" },
});
if (!argv.file) {
console.error("Error: Missing required argument --file");
process.exit(1);
}
const keyValuePairs = argv._;
const vars: Record<string, unknown> = keyValuePairs.reduce((acc, pair) => {
const [key, value] = pair.split("=");
if (key && value !== undefined) {
acc[key] = value; // Retain the `unknown` type for later flexibility
} else {
console.error(`Error: Invalid key=value pair "${pair}"`);
process.exit(1);
}
return acc;
}, {} as Record<string, unknown>);
return {
file: argv.file,
vars,
options: {},
};
}
async function main() {
const args = parseCliArgs();
await wrapper(args);
}
main();

View File

@@ -0,0 +1,2 @@
import './genaiscript-rust-shim';
export * from './genaiscript-rust-shim';

View File

@@ -0,0 +1,19 @@
{
"name": "@web-agent-rs/genaiscript-rust-shim",
"module": "index.ts",
"private": true,
"type": "module",
"scripts": {
"buildShim": "esbuild genaiscript-rust-shim.ts --bundle --format=esm --packages=external --outdir=dist --platform=node && chmod +x dist/genaiscript-rust-shim.js",
"setupDev": "cp dist/genaiscript-rust-shim.js ../../dist/genaiscript-rust-shim.js"
},
"devDependencies": {
"@types/bun": "latest",
"minimist": "^1.2.8",
"genaiscript": "^1.95.1",
"esbuild": "^0.24.2"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1,70 @@
type FenceFormat = "markdown" | "xml" | "none"
export interface WorkspaceFile {
/**
* Name of the file, relative to project root.
*/
filename: string
/**
* Content mime-type if known
*/
type?: string
/**
* Encoding of the content
*/
encoding?: "base64"
/**
* Content of the file.
*/
content?: string
}
export interface RunScriptOptions {
excludedFiles: string[]
excludeGitIgnore: boolean
runRetry: string
out: string
retry: string
retryDelay: string
maxDelay: string
json: boolean
yaml: boolean
outTrace: string
outOutput: string
outAnnotations: string
outChangelogs: string
pullRequest: string
pullRequestComment: string | boolean
pullRequestDescription: string | boolean
pullRequestReviews: boolean
outData: string
label: string
temperature: string | number
topP: string | number
seed: string | number
maxTokens: string | number
maxToolCalls: string | number
maxDataRepairs: string | number
model: string
smallModel: string
visionModel: string
embeddingsModel: string
modelAlias: string[]
provider: string
csvSeparator: string
cache: boolean | string
cacheName: string
applyEdits: boolean
failOnErrors: boolean
removeOut: boolean
vars: string[] | Record<string, string | boolean | number | object>
fallbackTools: boolean
jsSource: string
logprobs: boolean
topLogprobs: number
fenceFormat: FenceFormat
workspaceFiles?: WorkspaceFile[]
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -0,0 +1,4 @@
# auto-generated
genaiscript.d.ts
tsconfig.json
jsconfig.json

View File

@@ -0,0 +1,21 @@
import {types} from "mobx-state-tree";
import {QuoteStore} from "@web-agent-rs/core/quotes/models";
import {NewsStore} from "@web-agent-rs/core/news";
import newsStore from "./news";
import quoteStore from "./quotes";
const StateModel = types.model("State", {
symbols: types.array(types.string),
quotes: QuoteStore,
news: NewsStore,
});
const state = StateModel.create({
quotes: quoteStore,
news: newsStore,
});
export default state;

View File

@@ -0,0 +1,11 @@
import {NewsStore} from "@web-agent-rs/core/news";
import {Instance} from "mobx-state-tree";
const newsStore = NewsStore.create({
isLoading: false,
apiKey: process.env.PERIGON_API_KEY
});
export type NewsStore = Instance<typeof newsStore>;
export default newsStore;

View File

@@ -0,0 +1,7 @@
import {QuoteStore} from "@web-agent-rs/core/quotes/models";
const quoteStore = QuoteStore.create({
apiKey: process.env.CCC_API_KEY
});
export default quoteStore;

View File

@@ -0,0 +1,24 @@
import {PerigonClient as NewsSearchTool} from "@agentic/perigon";
script({
system: ["system.tools"],
tools: "agent",
maxTokens: 8192
})
const newSearchTool = new NewsSearchTool();
defTool(newSearchTool)
$`You are a chat assistant that uses agent tools to solve problems.
while true:
- ask the user for a question using the agent_user_input
- make a plan to answer the question step by step
- answer the question
end while
## guidance:
- use the agent tools to help you
- do NOT try to ask the user questions directly, use the agent_user_input tool instead.
`

View File

@@ -0,0 +1,281 @@
import {task, entrypoint, interrupt, MemorySaver} from "@langchain/langgraph"
import "./tools/searxng.genai.mjs"
import {SearxngClient} from "@agentic/searxng";
script({
title: "Deep Research Program",
description: "Researchers can use this program to conduct deep research on a topic",
model: "large",
cache: "ephemeral",
})
const {output, vars} = env
const breakdownResearch = task(
"breakdown_research",
async (question: string) => {
const result = await runPrompt(
async (ctx) => {
ctx.$`You are an expert research strategist.
Task: Break down the following research question into 3-5 focused sub-questions that would help comprehensively answer the main question.
Research question: ${question}
For each sub-question:
1. Assign a unique ID (e.g., SQ1, SQ2)
2. Explain the rationale for why this sub-question is important
3. Ensure the sub-questions collectively cover the main research question
Output the breakdown as a JSON object.`
},
{
label: "breakdown research",
responseSchema: {
type: "object",
properties: {
mainQuestion: {type: "string"},
subQuestions: {
type: "array",
items: {
type: "object",
properties: {
id: {type: "string"},
question: {type: "string"},
rationale: {type: "string"},
},
},
},
},
},
}
)
return result.json
}
)
const globalCtx = this;
const researchSubQuestion = task(
"research_subquestion",
async (subQuestion: { id: string; question: string }) => {
const searxng = new SearxngClient({apiBaseUrl: "https://search-engine-gsio.fly.dev"});
const {text} = await runPrompt(
(_) => {
_.defTool(searxng)
_.$`You are an expert researcher with access to comprehensive information.
Task: Thoroughly research the following question and provide a detailed answer.
Question ID: ${subQuestion.id}
Question: ${subQuestion.question}
Provide your findings in a structured format that includes:
- Your answer to the sub-question
- Relevant sources that support your answer
- Your confidence level in the answer (0-1)`
},
{
model: "small",
label: `research subquestion ${subQuestion.id}`,
maxDataRepairs: 2,
responseSchema: {
type: "object",
properties: {
subQuestionId: {type: "string"},
answer: {type: "string"},
sources: {
type: "array",
items: {
type: "object",
properties: {
title: {type: "string"},
url: {type: "string"},
relevance: {type: "string"},
},
},
},
confidence: {type: "number"},
},
},
}
)
return text
}
)
const synthesizeFindings = task(
"synthesize_findings",
async (mainQuestion: string, findings: any[]) => {
const result = await runPrompt(
async (ctx) => {
ctx.$`You are an expert research synthesizer.
Task: Synthesize the following research findings into a coherent response to the main research question.
Main Research Question: ${mainQuestion}
Findings:
${JSON.stringify(findings, null, 2)}
Provide a synthesis that:
1. Directly answers the main research question
2. Integrates the findings from all sub-questions
3. Identifies limitations in the current research
4. Suggests next steps for further investigation`
},
{
label: "synthesize findings",
responseType: "markdown",
responseSchema: {
type: "object",
properties: {
summary: {type: "string"},
findings: {type: "array", items: {type: "string"}},
limitations: {
type: "array",
items: {type: "string"},
},
nextSteps: {type: "array", items: {type: "string"}},
},
},
}
)
return result.json
}
)
const summarizeAndIdentifyGaps = task(
"summarize_and_identify_gaps",
async (synthesis: any, findings: any[]) => {
const result = await runPrompt(
async (ctx) => {
ctx.$`You are an expert research evaluator.
Task: Review the research synthesis and identify any gaps or areas that need deeper investigation.
Current synthesis:
${JSON.stringify(synthesis, null, 2)}
Research findings:
${JSON.stringify(findings, null, 2)}
Please provide:
1. A concise summary of current findings
2. Identify 2-3 specific knowledge gaps
3. Formulate follow-up questions to address these gaps`
},
{
label: "identify research gaps",
responseSchema: {
type: "object",
properties: {
summary: {type: "string"},
gaps: {
type: "array",
items: {type: "string"},
},
followUpQuestions: {
type: "array",
items: {
type: "object",
properties: {
id: {type: "string"},
question: {type: "string"},
},
},
},
},
},
}
)
return result.json
}
)
const researchWorkflow = entrypoint(
{checkpointer: new MemorySaver(), name: "research_workflow"},
async (input: { question: string; context?: string }) => {
const breakdown = await breakdownResearch(input.question)
const subQuestionFindings = []
for (const sq of breakdown.subQuestions) {
const analysis = await researchSubQuestion(sq);
console.log(analysis);
subQuestionFindings.push(analysis);
}
let synthesis = await synthesizeFindings(
input.question,
subQuestionFindings
)
const gapAnalysis = await summarizeAndIdentifyGaps(
synthesis,
subQuestionFindings
)
const followUpFindings = [];
for (const fq of gapAnalysis.followUpQuestions) {
const anwser = await researchSubQuestion(fq);
console.log(anwser);
followUpFindings.push(anwser);
}
const allFindings = [...subQuestionFindings, ...followUpFindings]
const finalSynthesis = await synthesizeFindings(
input.question,
allFindings
)
return {
question: input.question,
breakdown: breakdown,
initialFindings: subQuestionFindings,
gapAnalysis: gapAnalysis,
followUpFindings: followUpFindings,
synthesis: finalSynthesis,
}
}
)
const researchQuestion =
env.vars.question ||
"What are the most promising approaches to climate change mitigation?"
const threadId = `research-${Date.now()}`
const config = {
configurable: {
thread_id: threadId,
},
}
const results = await researchWorkflow.invoke(
{
question: researchQuestion,
context: vars.context || "",
},
config
)
output.fence(results, "json")

View File

@@ -0,0 +1,80 @@
import state from "./_state/index.js";
import {getSnapshot} from "mobx-state-tree";
import {collect_gainers_losers} from "@web-agent-rs/core/market";
def("QUERY", env.vars.user_input);
defTool(
"get_quote",
"Fetch quote for symbol",
{
"symbol": {
type: "string",
default: "BTC"
}
},
async (args) => {
const { symbol } = args;
await state.quotes.fetchQuote(symbol);
const quote = await state.quotes.getQuote(symbol);
return JSON.stringify(quote)
}
);
defTool(
"get_news",
"Fetches news for symbol",
{
"symbol": {
type: "string",
default: "BTC"
}
},
async (args) => {
const { symbol } = args;
await state.news.fetchNewsForSymbol(symbol, 5, "date");
const news = await state.news.getNewsForSymbol(symbol).map(i => getSnapshot(i));
return news
}
);
defTool(
"get_market",
"Fetches trending symbols of market",
{
"limit": {
type: "number",
default: "25"
}
},
async (args) => {
const { limit } = args;
const marketOverviewRequest = await collect_gainers_losers({apiKey: process.env.CCC_API_KEY, limit: parseInt(limit) })
return marketOverviewRequest.data.map(item => ({
symbol: item.symbol,
name: item.name,
change_1h: item.quote.USD.percent_change_1h,
price: item.quote.USD.price,
volume_24h: item.quote.USD.volume_24h
}))
}
);
$`You are a market data assistant specializing in financial analysis. Respond to QUERIES with accurate, clear, and concise information relevant to professionals in the finance sector. Use available tools efficiently to gather and present quantitative data.`;

View File

@@ -0,0 +1,11 @@
console.log("Generating image")
def("USER_INPUT", env.vars.user_input);
const inputs = {
host: JSON.parse(env.vars.user_input).host,
imageId: JSON.parse(env.vars.user_input).imageId
}
console.log(`![Generated Image](/generated/image/${inputs.imageId})`);

View File

@@ -0,0 +1,26 @@
script({
title: "Stock Market News Scraper",
tools: ["searxng"],
})
defTool({
"mcp-server-firecrawl": {
command: "npx",
args: ["-y", "firecrawl-mcp"],
},
})
def("QUERY_NEWS", "Latest news on AAPL")
def("QUERY_SENTIMENT", "Market sentiment for technology sector")
$`Search the query with searxng: QUERY_NEWS`
$`Scrape the top search result with firecrawl`
$`Search the query with searxng: QUERY_SENTIMENT`
$`Scrape the top search result with firecrawl`

View File

@@ -0,0 +1,24 @@
import {SearxngClient} from "@agentic/searxng";
import "./tools/searxng.genai.mjs"
script({
title: "news_search_agent",
tools: ["searxng"],
maxToolCalls: 2,
cache: false,
});
def("USER_INPUT", env.vars.user_input);
def("TODAY", new Date().toISOString().split("T")[0]);
def("LINK_FORMAT", "[Link](url)");
$`You are an assistant searching for news using complex queries to pinpoint results.
- tailor search to answer the question in USER_INPUT
- perform 2 searches in parallel sorted by relevance and date respectively
- create a markdown table of <=5 results of both searches
- header row: Date, Title, Summary, and Link
Respond with a single table, no extra text.`

View File

@@ -0,0 +1,18 @@
script({
isSystem: true
})
import {SearxngClient} from "@agentic/searxng";
import ky from 'ky';
const kyWithHeaders = ky.create({
referrerPolicy: "unsafe-url",
headers: {
'Authorization': 'Basic ' + btoa(`admin:${process.env.SEARXNG_PASSWORD}`),
}
});
const searxng = new SearxngClient({ky: kyWithHeaders});
defTool(searxng)

View File

@@ -0,0 +1,88 @@
import {Window} from 'happy-dom';
import {platform} from 'os';
script({
title: "scrape",
cache: false,
});
/*
"url": "Full URL in the conversation that references the URL being interacted with. No trailing slash!",
"query": "Implied question about the resources at the URL.",
"action": "read | scrape | crawl"
*/
try {
const {url, query, action} = JSON.parse(env.vars.user_input);
} catch (e) {
throw "Sorry! Something went wrong.";
}
const {url, query, action} = JSON.parse(env.vars.user_input);
def("URL", url);
def("QUERY", query);
def("ACTION", action);
// console.log({url, query, action});
if(!(new URL(url) ?? undefined)) {
throw "Bad URL. Maybe try again?"
}
function getBrowser(): "webkit" | "chromium" | "firefox" {
if (platform() === 'darwin') {
return "webkit"; // macOS is identified by 'darwin'
}
return "chromium"; // default to chromium for other platforms
}
const {text} = await host.fetchText(new URL(url).toString());
// const browser = getBrowser();
// const page = await host.browse(new URL(url).toString(), {
// browser: getBrowser(),
// headless: true,
// javaScriptEnabled: browser !== "chromium",
// // timeout: 3000,
// // bypassCSP: true,
// // baseUrl: new URL(url).origin,
// });
//
// const html = (await page.content());
// const title = (await page.title());
// console.log({html});
const window = new Window({
// url: "http://localhost:8080",
height: 1920,
width: 1080,
settings: {
navigator: {
userAgent: 'Mozilla/5.0 (compatible; GeoffsAI/1.0; +https://geoff.seemueller.io)',
},
}
});
window.document.body.innerHTML = text;
const textContent = window.document.body.textContent;
def("PAGE_TEXT", textContent);
$`You a helpful assistant interacting with resources found at the URL.
- markdown table is concise representation of PAGE_TEXT relevant to the QUERY
### Respond Example:
### Data from ${url}:
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Data 1 | Data 2 | Data 3 |
\n---[Example explanation of data significance to query.]
---
Respond with the markdown table and an explanation of significance. Do not include extra text.`;

View File

@@ -0,0 +1,28 @@
import {SearxngClient} from "@agentic/searxng";
import "./tools/searxng.genai.mjs"
script({
title: "web_search_agent",
maxTokens: 8192,
cache: false,
tools: ["searxng"],
});
def("USER_INPUT", env.vars.user_input);
def("LINK_FORMAT", "[Link](url)");
$`You are an assistant searching for web content using complex queries to pinpoint results.
- tailor search to answer the question in USER_INPUT
- perform 2 searches in parallel sorted by relevance and date respectively
- create a markdown table of <=5 results of both searches
- header row: Title, Description, and Link
Respond with a single table, no extra text.`

View File

@@ -0,0 +1,28 @@
{
"name": "@web-agent-rs/genaiscript",
"type": "module",
"workspaces": ["packages/*"],
"private": true,
"scripts": {
"dev": "cargo watch -x 'run src/main.rs'",
"ai:search": "genaiscript run genaisrc/web-search.genai.mts --vars USER_INPUT='who won the 2024 election?'",
"shim:ai:search": "pnpm build && ./dist/shim.js --file=genaisrc/search.genai.mts USER_INPUT=\"Who won the 2024 presidential election?\"\n",
"ai:news": "genaiscript run genaisrc/news-search.genai.mts --vars USER_INPUT='What are the latest updates and developments in the Ukraine war?'",
"ai:url:read": "genaiscript run genaisrc/web-scrape.genai.mts --vars USER_INPUT='{\"url\":\"https://geoff.seemueller.io/about\",\"query\":\"Describe the details of the page.\", \"action\": \"read\"}'",
"ai:url:scrape": "npx genaiscript run genaisrc/web-scrape.genai.mts --vars USER_INPUT='{\"url\":\"https://www.time4learning.com/homeschool-curriculum/high-school/eleventh-grade/math.html\",\"query\":\"What is on this page?\", \"action\": \"scrape\"}'",
"crypto:quote": "npx genaiscript run genaisrc/finance-query.genai.mts --vars USER_INPUT='Get a quote for BTC'",
"crypto:news": "npx genaiscript run genaisrc/finance-query.genai.mts --vars USER_INPUT='What is the news for Bitcoin?'",
"crypto:overview": "npx genaiscript run genaisrc/finance-query.genai.mts --vars USER_INPUT='What are the trending symbols in the market?'"
},
"dependencies": {
"@agentic/perigon": "^7.2.0",
"@agentic/searxng": "7.5.3",
"@kevinwatt/mcp-server-searxng": "^0.3.9",
"@types/node": "^22.10.2",
"genaiscript": "^1.95.1",
"happy-dom": "^16.0.1",
"@web-agent-rs/perigon": "workspace:*",
"@web-agent-rs/core": "workspace:*",
"ky": "^1.8.0"
}
}

106
packages/perigon/index.ts Normal file
View File

@@ -0,0 +1,106 @@
import type * as types from './types';
import type { ConfigOptions, FetchResponse } from 'api/dist/core'
import Oas from 'oas';
import APICore from 'api/dist/core';
import definition from './openapi.json';
class SDK {
spec: Oas;
core: APICore;
constructor() {
this.spec = Oas.init(definition);
this.core = new APICore(this.spec, 'perigon/unknown (api/6.1.3)');
}
/**
* Optionally configure various options that the SDK allows.
*
* @param config Object of supported SDK options and toggles.
* @param config.timeout Override the default `fetch` request timeout of 30 seconds. This number
* should be represented in milliseconds.
*/
config(config: ConfigOptions) {
this.core.setConfig(config);
}
/**
* If the API you're using requires authentication you can supply the required credentials
* through this method and the library will magically determine how they should be used
* within your API request.
*
* With the exception of OpenID and MutualTLS, it supports all forms of authentication
* supported by the OpenAPI specification.
*
* @example <caption>HTTP Basic auth</caption>
* sdk.auth('username', 'password');
*
* @example <caption>Bearer tokens (HTTP or OAuth 2)</caption>
* sdk.auth('myBearerToken');
*
* @example <caption>API Keys</caption>
* sdk.auth('myApiKey');
*
* @see {@link https://spec.openapis.org/oas/v3.0.3#fixed-fields-22}
* @see {@link https://spec.openapis.org/oas/v3.1.0#fixed-fields-22}
* @param values Your auth credentials for the API; can specify up to two strings or numbers.
*/
auth(...values: string[] | number[]) {
this.core.setAuth(...values);
return this;
}
/**
* If the API you're using offers alternate server URLs, and server variables, you can tell
* the SDK which one to use with this method. To use it you can supply either one of the
* server URLs that are contained within the OpenAPI definition (along with any server
* variables), or you can pass it a fully qualified URL to use (that may or may not exist
* within the OpenAPI definition).
*
* @example <caption>Server URL with server variables</caption>
* sdk.server('https://{region}.api.example.com/{basePath}', {
* name: 'eu',
* basePath: 'v14',
* });
*
* @example <caption>Fully qualified server URL</caption>
* sdk.server('https://eu.api.example.com/v14');
*
* @param url Server URL
* @param variables An object of variables to replace into the server URL.
*/
server(url: string, variables = {}) {
this.core.setServer(url, variables);
}
/**
* Search and filter all news articles available via the Perigon API. The result includes a
* list of individual articles that were matched to your specific criteria.
*
* @summary All Articles
* @throws FetchError<400, types.AllNewsResponse400> 400
* @throws FetchError<401, types.AllNewsResponse401> 401
* @throws FetchError<403, types.AllNewsResponse403> 403
* @throws FetchError<404, types.AllNewsResponse404> 404
* @throws FetchError<500, types.AllNewsResponse500> 500
*/
allNews(metadata: types.AllNewsMetadataParam): Promise<FetchResponse<200, types.AllNewsResponse200>> {
return this.core.fetch('/v1/all', 'get', metadata);
}
/**
* Stories
*
* @throws FetchError<400, types.Stories1Response400> 400
*/
stories1(metadata: types.Stories1MetadataParam): Promise<FetchResponse<200, types.Stories1Response200>> {
return this.core.fetch('/v1/stories/all', 'get', metadata);
}
}
const createSDK = (() => { return new SDK(); })()
;
export default createSDK;
export type { AllNewsMetadataParam, AllNewsResponse200, AllNewsResponse400, AllNewsResponse401, AllNewsResponse403, AllNewsResponse404, AllNewsResponse500, Stories1MetadataParam, Stories1Response200, Stories1Response400 } from './types';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
{
"name": "@web-agent-rs/perigon",
"version": "0.0.0",
"main": "index.ts",
"types": "./index.d.ts",
"type": "module",
"dependencies": {
"api": "^6.1.3",
"json-schema-to-ts": "^2.8.0-beta.0",
"oas": "^20.11.0"
}
}

File diff suppressed because one or more lines are too long

13
packages/perigon/types.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { FromSchema } from 'json-schema-to-ts';
import * as schemas from './schemas';
export type AllNewsMetadataParam = FromSchema<typeof schemas.AllNews.metadata>;
export type AllNewsResponse200 = FromSchema<typeof schemas.AllNews.response['200']>;
export type AllNewsResponse400 = FromSchema<typeof schemas.AllNews.response['400']>;
export type AllNewsResponse401 = FromSchema<typeof schemas.AllNews.response['401']>;
export type AllNewsResponse403 = FromSchema<typeof schemas.AllNews.response['403']>;
export type AllNewsResponse404 = FromSchema<typeof schemas.AllNews.response['404']>;
export type AllNewsResponse500 = FromSchema<typeof schemas.AllNews.response['500']>;
export type Stories1MetadataParam = FromSchema<typeof schemas.Stories1.metadata>;
export type Stories1Response200 = FromSchema<typeof schemas.Stories1.response['200']>;
export type Stories1Response400 = FromSchema<typeof schemas.Stories1.response['400']>;