├── examples ├── fxn-ag2 │ ├── .idea │ │ ├── .name │ │ ├── .gitignore │ │ ├── encodings.xml │ │ ├── vcs.xml │ │ ├── modules.xml │ │ └── fxn-ag2.iml │ ├── .DS_Store │ ├── .coverage │ ├── .env.template │ ├── src │ │ ├── __pycache__ │ │ │ ├── main.cpython-312.pyc │ │ │ └── bookkeeping_swarm.cpython-312.pyc │ │ ├── main.py │ │ └── bookkeeping_swarm.py │ ├── requirements-dev.txt │ ├── requirements.txt │ ├── tests │ │ ├── __pycache__ │ │ │ └── test_bookkeeping_swarm.cpython-312-pytest-7.4.3.pyc │ │ └── test_bookkeeping_swarm.py │ ├── run.sh │ ├── docker-compose.yml │ ├── Dockerfile │ └── README.md └── ag2-aag │ ├── .babelrc │ ├── agent-viz │ ├── src │ │ ├── index.css │ │ ├── App.jsx │ │ ├── main.jsx │ │ ├── App.css │ │ ├── assets │ │ │ └── react.svg │ │ └── components │ │ │ └── AgentVisualization.jsx │ ├── postcss.config.cjs.js │ ├── vite.config.js │ ├── tailwind.config.js │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── eslint.config.js │ └── public │ │ └── vite.svg │ ├── requirements.txt │ ├── LICENSE │ ├── tree_of_thoughts │ ├── web │ └── web_server.py │ ├── README.md │ ├── .gitignore │ ├── agent_network.py │ └── enhanced_reasoning_agent.py ├── dist ├── server │ ├── server │ │ └── src │ │ │ ├── index.d.ts │ │ │ └── index.js │ └── src │ │ ├── test │ │ ├── subscription-manager.d.ts │ │ └── subscription-manager.js │ │ ├── types │ │ ├── subscription_manager.js │ │ └── idl │ │ │ ├── idl.js │ │ │ └── idl.d.ts │ │ ├── config.d.ts │ │ ├── index.d.ts │ │ ├── config.js │ │ ├── index.js │ │ └── client │ │ └── fxn-solana-adapter.d.ts ├── config │ ├── types.js │ ├── validator.d.ts │ ├── networks.d.ts │ ├── index.d.ts │ ├── types.d.ts │ ├── validator.js │ ├── manager.d.ts │ ├── networks.js │ ├── index.js │ └── manager.js ├── types │ ├── subscription_manager.js │ └── idl │ │ ├── idl.js │ │ └── idl.d.ts ├── index.d.ts ├── index.js ├── config.d.ts ├── config.js └── client │ └── fxn-solana-adapter.d.ts ├── .gitignore ├── .DS_Store ├── server ├── tsconfig.json └── src │ └── index.ts ├── src ├── config │ ├── validator.ts │ ├── types.ts │ ├── index.ts │ ├── networks.ts │ └── manager.ts ├── index.ts ├── types │ ├── idl │ │ └── idl.ts │ └── subscription_manager.d.ts └── config.ts ├── tsconfig.json ├── package.json ├── python └── fxn_protocol │ ├── client.py │ └── README.md └── README.md /examples/fxn-ag2/.idea/.name: -------------------------------------------------------------------------------- 1 | fxn-ag2 -------------------------------------------------------------------------------- /dist/server/server/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/server/src/test/subscription-manager.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cache/* 2 | node_modules/* 3 | .idea/* 4 | .DS_Store 5 | -n 6 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oz-Networks/fxn-protocol-sdk/HEAD/.DS_Store -------------------------------------------------------------------------------- /examples/ag2-aag/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /dist/config/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /examples/fxn-ag2/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oz-Networks/fxn-protocol-sdk/HEAD/examples/fxn-ag2/.DS_Store -------------------------------------------------------------------------------- /examples/fxn-ag2/.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oz-Networks/fxn-protocol-sdk/HEAD/examples/fxn-ag2/.coverage -------------------------------------------------------------------------------- /dist/types/subscription_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/server/src/types/subscription_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /examples/fxn-ag2/.env.template: -------------------------------------------------------------------------------- 1 | # .env 2 | OPENAI_API_KEY=your_key_here 3 | SOLANA_PRIVATE_KEY=your_key_here 4 | FXN_SDK_PORT=3000 5 | -------------------------------------------------------------------------------- /dist/config/validator.d.ts: -------------------------------------------------------------------------------- 1 | export declare class AddressValidator { 2 | static validate(address: string, label: string): void; 3 | } 4 | -------------------------------------------------------------------------------- /examples/ag2-aag/requirements.txt: -------------------------------------------------------------------------------- 1 | ag2 2 | python-dotenv 3 | graphviz 4 | requests 5 | fastapi 6 | uvicorn[standard] 7 | websockets 8 | aiofiles -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/postcss.config.cjs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /examples/fxn-ag2/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /dist/config/networks.d.ts: -------------------------------------------------------------------------------- 1 | import { NetworkType, NetworkConfig } from './types'; 2 | export declare const NETWORK_CONFIGS: Record; 3 | -------------------------------------------------------------------------------- /examples/fxn-ag2/src/__pycache__/main.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oz-Networks/fxn-protocol-sdk/HEAD/examples/fxn-ag2/src/__pycache__/main.cpython-312.pyc -------------------------------------------------------------------------------- /examples/fxn-ag2/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/fxn-ag2/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # requirements-dev.txt 2 | aioresponses>=0.7.4 3 | pytest>=7.4.3 4 | pytest-asyncio>=0.21.1 5 | pytest-cov>=4.1.0 # for coverage reporting 6 | -------------------------------------------------------------------------------- /examples/fxn-ag2/requirements.txt: -------------------------------------------------------------------------------- 1 | # requirements.txt 2 | pyautogen[vision] 3 | solana 4 | base58 5 | requests 6 | pandas 7 | pillow 8 | python-dotenv 9 | aiohttp 10 | flaml[automl] 11 | -------------------------------------------------------------------------------- /examples/fxn-ag2/src/__pycache__/bookkeeping_swarm.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oz-Networks/fxn-protocol-sdk/HEAD/examples/fxn-ag2/src/__pycache__/bookkeeping_swarm.cpython-312.pyc -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/src/App.jsx: -------------------------------------------------------------------------------- 1 | import AgentVisualization from './components/AgentVisualization' 2 | 3 | function App() { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | export default App -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/fxn-ag2/tests/__pycache__/test_bookkeeping_swarm.cpython-312-pytest-7.4.3.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oz-Networks/fxn-protocol-sdk/HEAD/examples/fxn-ag2/tests/__pycache__/test_bookkeeping_swarm.cpython-312-pytest-7.4.3.pyc -------------------------------------------------------------------------------- /examples/fxn-ag2/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dist/config/index.d.ts: -------------------------------------------------------------------------------- 1 | import { NetworkConfig } from './types'; 2 | declare let config: NetworkConfig; 3 | export { config }; 4 | export * from './types'; 5 | export * from './networks'; 6 | export * from './validator'; 7 | export * from './manager'; 8 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } -------------------------------------------------------------------------------- /examples/fxn-ag2/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # run.sh 3 | 4 | # Build and start the containers 5 | docker-compose up --build -d 6 | 7 | # Wait for the FXN SDK server to start 8 | echo "Waiting for FXN SDK server to start..." 9 | sleep 10 10 | 11 | # Follow the logs 12 | docker-compose logs -f 13 | -------------------------------------------------------------------------------- /examples/fxn-ag2/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: '3.8' 3 | services: 4 | bookkeeping-swarm: 5 | build: . 6 | env_file: .env 7 | ports: 8 | - "3000:3000" 9 | volumes: 10 | - ./workspace:/app/workspace 11 | - ./fxn-protocol-sdk:/app/fxn-protocol-sdk 12 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /dist/server/src/config.d.ts: -------------------------------------------------------------------------------- 1 | export declare const config: { 2 | readonly subscriptionManagerAddress: "AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs"; 3 | readonly nftTokenAddress: "3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7"; 4 | readonly fxnMintAddress: "34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8"; 5 | }; 6 | -------------------------------------------------------------------------------- /dist/server/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { SolanaAdapter, type CreateSubscriptionParams, type RenewParams, type CancelParams, type SubscriptionState, type SubscriptionStatus, SubscriptionErrorCode, SubscriberDetails } from './client/fxn-solana-adapter'; 2 | export { type SubscriptionManager } from './types/subscription_manager'; 3 | -------------------------------------------------------------------------------- /examples/fxn-ag2/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/server", 5 | "rootDir": "..", 6 | "baseUrl": "..", 7 | "paths": { 8 | "@/*": ["src/*"] 9 | } 10 | }, 11 | "include": [ 12 | "../src/**/*", 13 | "src/**/*" 14 | ], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /src/config/validator.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | export class AddressValidator { 4 | static validate(address: string, label: string): void { 5 | try { 6 | new PublicKey(address); 7 | } catch (error) { 8 | throw new Error(`Invalid ${label}: ${address}`); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /dist/server/src/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.config = void 0; 4 | exports.config = { 5 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 6 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 7 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8' 8 | }; 9 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /dist/config/types.d.ts: -------------------------------------------------------------------------------- 1 | export type NetworkType = 'mainnet' | 'testnet' | 'devnet'; 2 | export interface NetworkConfig { 3 | subscriptionManagerAddress: string; 4 | nftTokenAddress: string; 5 | fxnMintAddress: string; 6 | rpcEndpoint: string; 7 | wsEndpoint?: string; 8 | } 9 | export interface SDKConfig { 10 | network: NetworkType; 11 | timeout?: number; 12 | commitment?: 'processed' | 'confirmed' | 'finalized'; 13 | } 14 | -------------------------------------------------------------------------------- /src/config/types.ts: -------------------------------------------------------------------------------- 1 | export type NetworkType = 'mainnet' | 'testnet' | 'devnet'; 2 | 3 | export interface NetworkConfig { 4 | subscriptionManagerAddress: string; 5 | nftTokenAddress: string; 6 | fxnMintAddress: string; 7 | rpcEndpoint: string; 8 | wsEndpoint?: string; 9 | } 10 | 11 | export interface SDKConfig { 12 | network: NetworkType; 13 | timeout?: number; 14 | commitment?: 'processed' | 'confirmed' | 'finalized'; 15 | } -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { SolanaAdapter, type CreateSubscriptionParams, type RenewParams, type CancelParams, type SubscriptionState, type SubscriptionStatus, type SubscriberDetails, type SetDataProviderFeeParams, type RequestSubscriptionParams, type ApproveSubscriptionRequestParams, type SubscriptionListParams, type AgentParams, type AgentProfile, type RequestStruct, type QualityInfoParams, SubscriptionErrorCode, } from './client/fxn-solana-adapter'; 2 | export { type SubscriptionManager } from './types/subscription_manager'; 3 | -------------------------------------------------------------------------------- /dist/config/validator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.AddressValidator = void 0; 4 | const web3_js_1 = require("@solana/web3.js"); 5 | class AddressValidator { 6 | static validate(address, label) { 7 | try { 8 | new web3_js_1.PublicKey(address); 9 | } 10 | catch (error) { 11 | throw new Error(`Invalid ${label}: ${address}`); 12 | } 13 | } 14 | } 15 | exports.AddressValidator = AddressValidator; 16 | -------------------------------------------------------------------------------- /examples/fxn-ag2/.idea/fxn-ag2.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.SubscriptionErrorCode = exports.SolanaAdapter = void 0; 4 | // Export the main adapter 5 | var fxn_solana_adapter_1 = require("./client/fxn-solana-adapter"); 6 | Object.defineProperty(exports, "SolanaAdapter", { enumerable: true, get: function () { return fxn_solana_adapter_1.SolanaAdapter; } }); 7 | Object.defineProperty(exports, "SubscriptionErrorCode", { enumerable: true, get: function () { return fxn_solana_adapter_1.SubscriptionErrorCode; } }); 8 | -------------------------------------------------------------------------------- /dist/server/src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.SubscriptionErrorCode = exports.SolanaAdapter = void 0; 4 | // Export the main adapter 5 | var fxn_solana_adapter_1 = require("./client/fxn-solana-adapter"); 6 | Object.defineProperty(exports, "SolanaAdapter", { enumerable: true, get: function () { return fxn_solana_adapter_1.SolanaAdapter; } }); 7 | Object.defineProperty(exports, "SubscriptionErrorCode", { enumerable: true, get: function () { return fxn_solana_adapter_1.SubscriptionErrorCode; } }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "rootDirs": ["src"], 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": ["src/*"] 17 | } 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules", "dist", "src/test"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agent-viz", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "framer-motion": "^11.15.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.43", 18 | "@types/react-dom": "^18.2.17", 19 | "@vitejs/plugin-react": "^4.2.1", 20 | "autoprefixer": "^10.4.16", 21 | "postcss": "^8.4.32", 22 | "tailwindcss": "^3.3.6", 23 | "vite": "^5.0.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dist/config/manager.d.ts: -------------------------------------------------------------------------------- 1 | import { SDKConfig, NetworkConfig } from './types'; 2 | export declare class ConfigurationManager { 3 | private static instance; 4 | private currentConfig; 5 | private networkConfig; 6 | private constructor(); 7 | static initialize(config: SDKConfig): ConfigurationManager; 8 | static getInstance(): ConfigurationManager; 9 | private validateAddresses; 10 | getConfig(): SDKConfig; 11 | getNetworkConfig(): NetworkConfig; 12 | updateConfig(config: Partial): void; 13 | } 14 | export declare const initializeConfig: (config: SDKConfig) => ConfigurationManager; 15 | export declare const getConfig: () => ConfigurationManager; 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Export the main adapter 2 | export { 3 | SolanaAdapter, 4 | type CreateSubscriptionParams, 5 | type RenewParams, 6 | type CancelParams, 7 | type SubscriptionState, 8 | type SubscriptionStatus, 9 | type SubscriberDetails, 10 | type SetDataProviderFeeParams, 11 | type RequestSubscriptionParams, 12 | type ApproveSubscriptionRequestParams, 13 | type SubscriptionListParams, 14 | type AgentParams, 15 | type AgentProfile, 16 | type RequestStruct, 17 | type QualityInfoParams, 18 | SubscriptionErrorCode, 19 | } from './client/fxn-solana-adapter'; 20 | 21 | // Export types 22 | export { type SubscriptionManager } from './types/subscription_manager'; 23 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // src/config/index.ts 2 | import { getConfig, initializeConfig } from './manager'; 3 | import { NetworkConfig } from './types'; 4 | 5 | // Create config with fallback initialization 6 | let config: NetworkConfig; 7 | try { 8 | config = getConfig().getNetworkConfig(); 9 | } catch { 10 | // If not initialized, initialize with defaults 11 | config = initializeConfig({ 12 | network: 'devnet', 13 | timeout: 30000, 14 | commitment: 'confirmed' 15 | }).getNetworkConfig(); 16 | } 17 | 18 | // Export the config 19 | export { config }; 20 | 21 | // Export everything else 22 | export * from './types'; 23 | export * from './networks'; 24 | export * from './validator'; 25 | export * from './manager'; 26 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /examples/fxn-ag2/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.10-slim 3 | 4 | # Install Node.js for the FXN protocol SDK 5 | RUN apt-get update && apt-get install -y \ 6 | git \ 7 | nodejs \ 8 | npm \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # Set working directory 12 | WORKDIR /app 13 | 14 | # Copy FXN protocol SDK and install dependencies 15 | COPY fxn-protocol-sdk /app/fxn-protocol-sdk 16 | WORKDIR /app/fxn-protocol-sdk 17 | RUN npm install 18 | RUN npm run build 19 | 20 | # Back to app directory 21 | WORKDIR /app 22 | 23 | # Copy Python requirements and install 24 | COPY requirements.txt . 25 | RUN pip install --no-cache-dir -r requirements.txt 26 | 27 | # Copy source code 28 | COPY src/ /app/src/ 29 | 30 | # Copy .env file 31 | COPY .env . 32 | 33 | # Expose port for FXN client 34 | EXPOSE 3000 35 | 36 | # Start the application 37 | CMD ["python", "-m", "src.main"] 38 | -------------------------------------------------------------------------------- /dist/config.d.ts: -------------------------------------------------------------------------------- 1 | export type NetworkType = 'mainnet' | 'testnet' | 'devnet'; 2 | export interface NetworkConfig { 3 | subscriptionManagerAddress: string; 4 | nftTokenAddress: string; 5 | fxnMintAddress: string; 6 | rpcEndpoint: string; 7 | wsEndpoint?: string; 8 | } 9 | export interface SDKConfig { 10 | network: NetworkType; 11 | timeout?: number; 12 | commitment?: 'processed' | 'confirmed' | 'finalized'; 13 | } 14 | export declare class ConfigurationManager { 15 | private static instance; 16 | private currentConfig; 17 | private networkConfig; 18 | private constructor(); 19 | static initialize(config: SDKConfig): ConfigurationManager; 20 | static getInstance(): ConfigurationManager; 21 | private validateAddresses; 22 | getConfig(): SDKConfig; 23 | getNetworkConfig(): NetworkConfig; 24 | updateConfig(config: Partial): void; 25 | } 26 | export declare const initializeConfig: (config: SDKConfig) => ConfigurationManager; 27 | export declare const getConfig: () => ConfigurationManager; 28 | -------------------------------------------------------------------------------- /src/types/idl/idl.ts: -------------------------------------------------------------------------------- 1 | // idl.ts 2 | import IDL_JSON from './subscription_manager.json'; 3 | 4 | // Helper function to convert account properties 5 | function convertAccounts(accounts: any[]) { 6 | return accounts.map(account => ({ 7 | name: account.name, 8 | isMut: account.writable ?? false, 9 | isSigner: account.signer ?? false, 10 | // Preserve PDA info if it exists 11 | pda: account.pda, 12 | // Preserve address if it exists 13 | address: account.address 14 | })); 15 | } 16 | 17 | // Transform instructions to match Anchor's expected format 18 | const instructions = IDL_JSON.instructions.map(instruction => ({ 19 | ...instruction, 20 | accounts: convertAccounts(instruction.accounts) 21 | })); 22 | 23 | // Create the properly formatted IDL 24 | export const IDL = { 25 | version: IDL_JSON.metadata.version, 26 | name: IDL_JSON.metadata.name, 27 | instructions, 28 | accounts: IDL_JSON.accounts, 29 | events: IDL_JSON.events, 30 | errors: IDL_JSON.errors, 31 | types: IDL_JSON.types, 32 | }; 33 | -------------------------------------------------------------------------------- /examples/ag2-aag/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /src/config/networks.ts: -------------------------------------------------------------------------------- 1 | import { NetworkType, NetworkConfig } from './types'; 2 | 3 | export const NETWORK_CONFIGS: Record = { 4 | mainnet: { 5 | subscriptionManagerAddress: '7grtCnm6TmUiB4a6b4roSiVzZCQ5agSz9aj8aYJiWpKE', 6 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 7 | fxnMintAddress: '92cRC6kV5D7TiHX1j56AbkPbffo9jwcXxSDQZ8Mopump', 8 | rpcEndpoint: 'https://api.mainnet-beta.solana.com', 9 | wsEndpoint: 'wss://api.mainnet-beta.solana.com' 10 | }, 11 | testnet: { 12 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 13 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 14 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 15 | rpcEndpoint: 'https://api.testnet.solana.com', 16 | wsEndpoint: 'wss://api.testnet.solana.com' 17 | }, 18 | devnet: { 19 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 20 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 21 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 22 | rpcEndpoint: 'https://api.devnet.solana.com', 23 | wsEndpoint: 'wss://api.devnet.solana.com' 24 | } 25 | }; -------------------------------------------------------------------------------- /dist/config/networks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.NETWORK_CONFIGS = void 0; 4 | exports.NETWORK_CONFIGS = { 5 | mainnet: { 6 | subscriptionManagerAddress: '7grtCnm6TmUiB4a6b4roSiVzZCQ5agSz9aj8aYJiWpKE', 7 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 8 | fxnMintAddress: '92cRC6kV5D7TiHX1j56AbkPbffo9jwcXxSDQZ8Mopump', 9 | rpcEndpoint: 'https://api.mainnet-beta.solana.com', 10 | wsEndpoint: 'wss://api.mainnet-beta.solana.com' 11 | }, 12 | testnet: { 13 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 14 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 15 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 16 | rpcEndpoint: 'https://api.testnet.solana.com', 17 | wsEndpoint: 'wss://api.testnet.solana.com' 18 | }, 19 | devnet: { 20 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 21 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 22 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 23 | rpcEndpoint: 'https://api.devnet.solana.com', 24 | wsEndpoint: 'wss://api.devnet.solana.com' 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /examples/ag2-aag/tree_of_thoughts: -------------------------------------------------------------------------------- 1 | // Tree of Thoughts 2 | digraph { 3 | rankdir=TB 4 | 0 [label="exitcode: 0 (execution succeeded) 5 | Code output: 6 | 3 7 | visits: 0 8 | value: 0"] 9 | "0_0" [label="Request more context or information about the code... 10 | visits: 0 11 | value: 0.4444444444444444"] 12 | 0 -> "0_0" 13 | "0_1" [label="Suggest analyzing the output: Given the output is ... 14 | visits: 0 15 | value: 0.7777777777777778"] 16 | "0_1_0" [label="Uncover the specific function or lines of code tha... 17 | visits: 0 18 | value: 0.7777777777777778"] 19 | "0_1" -> "0_1_0" 20 | "0_1_1" [label="Understand the user's question or problem statemen... 21 | visits: 0 22 | value: 0.3333333333333333"] 23 | "0_1" -> "0_1_1" 24 | "0_1_2" [label="Propose detailed analysis methods for the output \"... 25 | visits: 0 26 | value: 0.6666666666666666"] 27 | "0_1" -> "0_1_2" 28 | "0_1_3" [label="Research potential significance of the output in s... 29 | visits: 0 30 | value: 0.5555555555555556"] 31 | "0_1" -> "0_1_3" 32 | "0_1_4" [label="TERMINATE. While concluding the process isn't usua... 33 | visits: 0 34 | value: 1.0"] 35 | "0_1" -> "0_1_4" 36 | 0 -> "0_1" 37 | "0_2" [label="Propose checking the rest of the code: By looking ... 38 | visits: 0 39 | value: 0.5555555555555556"] 40 | 0 -> "0_2" 41 | "0_3" [label="Recommend running additional tests or increasing t... 42 | visits: 0 43 | value: 0.2222222222222222"] 44 | 0 -> "0_3" 45 | } 46 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/config/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 14 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | exports.config = void 0; 18 | // src/config/index.ts 19 | const manager_1 = require("./manager"); 20 | // Create config with fallback initialization 21 | let config; 22 | try { 23 | exports.config = config = (0, manager_1.getConfig)().getNetworkConfig(); 24 | } 25 | catch (_a) { 26 | // If not initialized, initialize with defaults 27 | exports.config = config = (0, manager_1.initializeConfig)({ 28 | network: 'devnet', 29 | timeout: 30000, 30 | commitment: 'confirmed' 31 | }).getNetworkConfig(); 32 | } 33 | // Export everything else 34 | __exportStar(require("./types"), exports); 35 | __exportStar(require("./networks"), exports); 36 | __exportStar(require("./validator"), exports); 37 | __exportStar(require("./manager"), exports); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fxn-protocol-sdk", 3 | "version": "0.0.1", 4 | "description": "Utility functions to access FXN incentivized peer-to-peer messaging protocol - Devnet", 5 | "engines": { 6 | "node": ">=22.11.0" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Oz-Networks/fxn-protocol-sdk.git" 11 | }, 12 | "keywords": [ 13 | "fxn", 14 | "p2p", 15 | "blockchain", 16 | "messaging" 17 | ], 18 | "main": "dist/index.js", 19 | "module": "dist/index.js", 20 | "types": "dist/index.d.ts", 21 | "files": [ 22 | "dist" 23 | ], 24 | "scripts": { 25 | "build": "tsc", 26 | "prepare": "npm run build", 27 | "prepublishOnly": "npm run build", 28 | "build:server": "tsc -p server/tsconfig.json && tsc", 29 | "start": "node dist/server/server/src/index.js", 30 | "dev": "ts-node -P server/tsconfig.json server/src/index.ts" 31 | }, 32 | "author": "FXN", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/Oz-Networks/fxn-protocol-sdk.git" 36 | }, 37 | "homepage": "https://fxn.world", 38 | "dependencies": { 39 | "@coral-xyz/anchor": "^0.30.1", 40 | "@solana/spl-token": "^0.3.8", 41 | "@solana/web3.js": "^1.87.0", 42 | "dist": "^0.1.2", 43 | "express": "^4.21.2", 44 | "mocha": "^11.0.1", 45 | "ts-node": "^10.9.2" 46 | }, 47 | "devDependencies": { 48 | "@types/chai": "^4.3.5", 49 | "@types/express": "^5.0.0", 50 | "@types/mocha": "^10.0.10", 51 | "chai": "^4.3.7", 52 | "mocha": "^11.0.1", 53 | "typescript": "^5.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /dist/types/idl/idl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.IDL = void 0; 7 | // idl.ts 8 | const subscription_manager_json_1 = __importDefault(require("./subscription_manager.json")); 9 | // Helper function to convert account properties 10 | function convertAccounts(accounts) { 11 | return accounts.map(account => { 12 | var _a, _b; 13 | return ({ 14 | name: account.name, 15 | isMut: (_a = account.writable) !== null && _a !== void 0 ? _a : false, 16 | isSigner: (_b = account.signer) !== null && _b !== void 0 ? _b : false, 17 | // Preserve PDA info if it exists 18 | pda: account.pda, 19 | // Preserve address if it exists 20 | address: account.address 21 | }); 22 | }); 23 | } 24 | // Transform instructions to match Anchor's expected format 25 | const instructions = subscription_manager_json_1.default.instructions.map(instruction => (Object.assign(Object.assign({}, instruction), { accounts: convertAccounts(instruction.accounts) }))); 26 | // Create the properly formatted IDL 27 | exports.IDL = { 28 | version: subscription_manager_json_1.default.metadata.version, 29 | name: subscription_manager_json_1.default.metadata.name, 30 | instructions, 31 | accounts: subscription_manager_json_1.default.accounts, 32 | events: subscription_manager_json_1.default.events, 33 | errors: subscription_manager_json_1.default.errors, 34 | types: subscription_manager_json_1.default.types, 35 | }; 36 | -------------------------------------------------------------------------------- /dist/server/src/types/idl/idl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.IDL = void 0; 7 | // idl.ts 8 | const subscription_manager_json_1 = __importDefault(require("./subscription_manager.json")); 9 | // Helper function to convert account properties 10 | function convertAccounts(accounts) { 11 | return accounts.map(account => { 12 | var _a, _b; 13 | return ({ 14 | name: account.name, 15 | isMut: (_a = account.writable) !== null && _a !== void 0 ? _a : false, 16 | isSigner: (_b = account.signer) !== null && _b !== void 0 ? _b : false, 17 | // Preserve PDA info if it exists 18 | pda: account.pda, 19 | // Preserve address if it exists 20 | address: account.address 21 | }); 22 | }); 23 | } 24 | // Transform instructions to match Anchor's expected format 25 | const instructions = subscription_manager_json_1.default.instructions.map(instruction => (Object.assign(Object.assign({}, instruction), { accounts: convertAccounts(instruction.accounts) }))); 26 | // Create the properly formatted IDL 27 | exports.IDL = { 28 | version: subscription_manager_json_1.default.metadata.version, 29 | name: subscription_manager_json_1.default.metadata.name, 30 | instructions, 31 | accounts: subscription_manager_json_1.default.accounts, 32 | events: subscription_manager_json_1.default.events, 33 | errors: subscription_manager_json_1.default.errors, 34 | types: subscription_manager_json_1.default.types, 35 | }; 36 | -------------------------------------------------------------------------------- /examples/fxn-ag2/src/main.py: -------------------------------------------------------------------------------- 1 | # src/main.py 2 | import os 3 | import asyncio 4 | import signal 5 | from dotenv import load_dotenv 6 | from pathlib import Path 7 | from src.bookkeeping_swarm import BookkeepingSwarm 8 | 9 | # Load .env file 10 | load_dotenv() 11 | 12 | async def main(): 13 | # Validate required environment variables 14 | wallet_private_key = os.getenv("SOLANA_PRIVATE_KEY") 15 | if not wallet_private_key: 16 | raise ValueError("SOLANA_PRIVATE_KEY not found in environment variables") 17 | 18 | # Initialize the swarm 19 | swarm = BookkeepingSwarm( 20 | wallet_private_key=wallet_private_key, 21 | port=int(os.getenv("FXN_SDK_PORT", "3000")) 22 | ) 23 | 24 | # Setup graceful shutdown 25 | loop = asyncio.get_running_loop() 26 | stop_event = asyncio.Event() 27 | 28 | def signal_handler(): 29 | print("\nShutdown signal received. Cleaning up...") 30 | stop_event.set() 31 | 32 | # Register signal handlers 33 | for sig in (signal.SIGINT, signal.SIGTERM): 34 | asyncio.get_running_loop().add_signal_handler(sig, signal_handler) 35 | 36 | try: 37 | # Create task for the polling loop 38 | polling_task = asyncio.create_task(swarm.poll_subscribers_loop()) 39 | 40 | # Wait for shutdown signal 41 | await stop_event.wait() 42 | 43 | # Cancel the polling task 44 | polling_task.cancel() 45 | try: 46 | await polling_task 47 | except asyncio.CancelledError: 48 | pass 49 | 50 | finally: 51 | # Ensure cleanup happens 52 | await swarm.stop() 53 | print("Shutdown complete") 54 | 55 | if __name__ == "__main__": 56 | try: 57 | asyncio.run(main()) 58 | except KeyboardInterrupt: 59 | print("\nProcess interrupted by user") 60 | -------------------------------------------------------------------------------- /dist/server/src/types/idl/idl.d.ts: -------------------------------------------------------------------------------- 1 | export declare const IDL: { 2 | version: string; 3 | name: string; 4 | instructions: ({ 5 | accounts: { 6 | name: any; 7 | isMut: any; 8 | isSigner: any; 9 | pda: any; 10 | address: any; 11 | }[]; 12 | name: string; 13 | discriminator: number[]; 14 | args: never[]; 15 | returns: { 16 | vec: string; 17 | }; 18 | } | { 19 | accounts: { 20 | name: any; 21 | isMut: any; 22 | isSigner: any; 23 | pda: any; 24 | address: any; 25 | }[]; 26 | name: string; 27 | discriminator: number[]; 28 | args: { 29 | name: string; 30 | type: string; 31 | }[]; 32 | returns?: undefined; 33 | } | { 34 | accounts: { 35 | name: any; 36 | isMut: any; 37 | isSigner: any; 38 | pda: any; 39 | address: any; 40 | }[]; 41 | name: string; 42 | discriminator: number[]; 43 | args: { 44 | name: string; 45 | type: string; 46 | }[]; 47 | returns?: undefined; 48 | })[]; 49 | accounts: { 50 | name: string; 51 | discriminator: number[]; 52 | }[]; 53 | events: { 54 | name: string; 55 | discriminator: number[]; 56 | }[]; 57 | errors: { 58 | code: number; 59 | name: string; 60 | msg: string; 61 | }[]; 62 | types: ({ 63 | name: string; 64 | type: { 65 | kind: string; 66 | fields: ({ 67 | name: string; 68 | type: string; 69 | } | { 70 | name: string; 71 | type: { 72 | vec: { 73 | defined: { 74 | name: string; 75 | }; 76 | }; 77 | }; 78 | })[]; 79 | }; 80 | } | { 81 | name: string; 82 | type: { 83 | kind: string; 84 | fields: { 85 | name: string; 86 | type: { 87 | vec: string; 88 | }; 89 | }[]; 90 | }; 91 | })[]; 92 | }; 93 | -------------------------------------------------------------------------------- /dist/config/manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.getConfig = exports.initializeConfig = exports.ConfigurationManager = void 0; 4 | const networks_1 = require("./networks"); 5 | const validator_1 = require("./validator"); 6 | class ConfigurationManager { 7 | constructor(config) { 8 | this.currentConfig = Object.assign(Object.assign({}, config), { timeout: config.timeout || 30000, commitment: config.commitment || 'confirmed' }); 9 | this.networkConfig = networks_1.NETWORK_CONFIGS[config.network]; 10 | this.validateAddresses(); 11 | } 12 | static initialize(config) { 13 | if (!ConfigurationManager.instance) { 14 | ConfigurationManager.instance = new ConfigurationManager(config); 15 | } 16 | return ConfigurationManager.instance; 17 | } 18 | static getInstance() { 19 | if (!ConfigurationManager.instance) { 20 | // Fallback to devnet config for backward compatibility 21 | return ConfigurationManager.initialize({ 22 | network: 'mainnet', 23 | timeout: 30000, 24 | commitment: 'confirmed' 25 | }); 26 | } 27 | return ConfigurationManager.instance; 28 | } 29 | validateAddresses() { 30 | validator_1.AddressValidator.validate(this.networkConfig.subscriptionManagerAddress, 'subscription manager address'); 31 | validator_1.AddressValidator.validate(this.networkConfig.nftTokenAddress, 'NFT token address'); 32 | validator_1.AddressValidator.validate(this.networkConfig.fxnMintAddress, 'FXN mint address'); 33 | } 34 | getConfig() { 35 | return Object.assign({}, this.currentConfig); 36 | } 37 | getNetworkConfig() { 38 | return Object.assign({}, this.networkConfig); 39 | } 40 | updateConfig(config) { 41 | if (config.network && config.network !== this.currentConfig.network) { 42 | this.networkConfig = networks_1.NETWORK_CONFIGS[config.network]; 43 | this.validateAddresses(); 44 | } 45 | this.currentConfig = Object.assign(Object.assign({}, this.currentConfig), config); 46 | } 47 | } 48 | exports.ConfigurationManager = ConfigurationManager; 49 | const initializeConfig = (config) => { 50 | return ConfigurationManager.initialize(config); 51 | }; 52 | exports.initializeConfig = initializeConfig; 53 | const getConfig = () => { 54 | return ConfigurationManager.getInstance(); 55 | }; 56 | exports.getConfig = getConfig; 57 | -------------------------------------------------------------------------------- /dist/types/idl/idl.d.ts: -------------------------------------------------------------------------------- 1 | export declare const IDL: { 2 | version: string; 3 | name: string; 4 | instructions: ({ 5 | accounts: { 6 | name: any; 7 | isMut: any; 8 | isSigner: any; 9 | pda: any; 10 | address: any; 11 | }[]; 12 | name: string; 13 | discriminator: number[]; 14 | args: never[]; 15 | returns: { 16 | vec: string; 17 | }; 18 | } | { 19 | accounts: { 20 | name: any; 21 | isMut: any; 22 | isSigner: any; 23 | pda: any; 24 | address: any; 25 | }[]; 26 | name: string; 27 | discriminator: number[]; 28 | args: ({ 29 | name: string; 30 | type: string; 31 | } | { 32 | name: string; 33 | type: { 34 | vec: string; 35 | }; 36 | })[]; 37 | returns?: undefined; 38 | } | { 39 | accounts: { 40 | name: any; 41 | isMut: any; 42 | isSigner: any; 43 | pda: any; 44 | address: any; 45 | }[]; 46 | name: string; 47 | discriminator: number[]; 48 | args: { 49 | name: string; 50 | type: string; 51 | }[]; 52 | returns?: undefined; 53 | })[]; 54 | accounts: { 55 | name: string; 56 | discriminator: number[]; 57 | }[]; 58 | events: { 59 | name: string; 60 | discriminator: number[]; 61 | }[]; 62 | errors: { 63 | code: number; 64 | name: string; 65 | msg: string; 66 | }[]; 67 | types: ({ 68 | name: string; 69 | type: { 70 | kind: string; 71 | fields: ({ 72 | name: string; 73 | type: string; 74 | } | { 75 | name: string; 76 | type: { 77 | vec: string; 78 | }; 79 | })[]; 80 | }; 81 | } | { 82 | name: string; 83 | type: { 84 | kind: string; 85 | fields: ({ 86 | name: string; 87 | type: string; 88 | } | { 89 | name: string; 90 | type: { 91 | vec: { 92 | defined: { 93 | name: string; 94 | }; 95 | }; 96 | }; 97 | })[]; 98 | }; 99 | })[]; 100 | }; 101 | -------------------------------------------------------------------------------- /src/config/manager.ts: -------------------------------------------------------------------------------- 1 | import { SDKConfig, NetworkConfig } from './types'; 2 | import { NETWORK_CONFIGS } from './networks'; 3 | import { AddressValidator } from './validator'; 4 | 5 | export class ConfigurationManager { 6 | private static instance: ConfigurationManager; 7 | private currentConfig: SDKConfig; 8 | private networkConfig: NetworkConfig; 9 | 10 | private constructor(config: SDKConfig) { 11 | this.currentConfig = { 12 | ...config, 13 | timeout: config.timeout || 30000, 14 | commitment: config.commitment || 'confirmed' 15 | }; 16 | this.networkConfig = NETWORK_CONFIGS[config.network]; 17 | this.validateAddresses(); 18 | } 19 | 20 | public static initialize(config: SDKConfig): ConfigurationManager { 21 | if (!ConfigurationManager.instance) { 22 | ConfigurationManager.instance = new ConfigurationManager(config); 23 | } 24 | return ConfigurationManager.instance; 25 | } 26 | 27 | public static getInstance(): ConfigurationManager { 28 | if (!ConfigurationManager.instance) { 29 | // Fallback to devnet config for backward compatibility 30 | return ConfigurationManager.initialize({ 31 | network: 'mainnet', 32 | timeout: 30000, 33 | commitment: 'confirmed' 34 | }); 35 | } 36 | return ConfigurationManager.instance; 37 | } 38 | 39 | private validateAddresses(): void { 40 | AddressValidator.validate(this.networkConfig.subscriptionManagerAddress, 'subscription manager address'); 41 | AddressValidator.validate(this.networkConfig.nftTokenAddress, 'NFT token address'); 42 | AddressValidator.validate(this.networkConfig.fxnMintAddress, 'FXN mint address'); 43 | } 44 | 45 | public getConfig(): SDKConfig { 46 | return { ...this.currentConfig }; 47 | } 48 | 49 | public getNetworkConfig(): NetworkConfig { 50 | return { ...this.networkConfig }; 51 | } 52 | 53 | public updateConfig(config: Partial): void { 54 | if (config.network && config.network !== this.currentConfig.network) { 55 | this.networkConfig = NETWORK_CONFIGS[config.network]; 56 | this.validateAddresses(); 57 | } 58 | this.currentConfig = { 59 | ...this.currentConfig, 60 | ...config 61 | }; 62 | } 63 | } 64 | 65 | export const initializeConfig = (config: SDKConfig): ConfigurationManager => { 66 | return ConfigurationManager.initialize(config); 67 | }; 68 | 69 | export const getConfig = (): ConfigurationManager => { 70 | return ConfigurationManager.getInstance(); 71 | }; 72 | -------------------------------------------------------------------------------- /dist/server/src/client/fxn-solana-adapter.d.ts: -------------------------------------------------------------------------------- 1 | import { Program, AnchorProvider, IdlAccounts, BN } from '@coral-xyz/anchor'; 2 | import { PublicKey, TransactionSignature } from '@solana/web3.js'; 3 | import type { SubscriptionManager } from '../types/subscription_manager'; 4 | export interface RenewParams { 5 | dataProvider: PublicKey; 6 | newRecipient: string; 7 | newEndTime: number; 8 | qualityScore: number; 9 | nftTokenAccount: PublicKey; 10 | } 11 | export interface CancelParams { 12 | dataProvider: PublicKey; 13 | qualityScore: number; 14 | nftTokenAccount?: PublicKey; 15 | } 16 | export interface SubscriptionState { 17 | endTime: BN; 18 | recipient: string; 19 | } 20 | export interface SubscriberDetails { 21 | subscriber: PublicKey; 22 | subscriptionPDA: PublicKey; 23 | subscription: { 24 | endTime: BN; 25 | recipient: string; 26 | }; 27 | status: 'active' | 'expired' | 'expiring_soon'; 28 | } 29 | export interface SetDataProviderFeeParams { 30 | fee: number; 31 | } 32 | type QualityInfoAccount = IdlAccounts['qualityInfo']; 33 | type SubscriptionAccount = IdlAccounts['subscription']; 34 | export declare enum SubscriptionErrorCode { 35 | PeriodTooShort = 6000, 36 | AlreadySubscribed = 6001, 37 | InsufficientPayment = 6002, 38 | InvalidNFTHolder = 6003, 39 | SubscriptionNotFound = 6004, 40 | QualityOutOfRange = 6005, 41 | SubscriptionAlreadyEnded = 6006, 42 | ActiveSubscription = 6007, 43 | NotOwner = 6008 44 | } 45 | export interface CreateSubscriptionParams { 46 | dataProvider: PublicKey; 47 | recipient: string; 48 | durationInDays: number; 49 | nftTokenAccount: PublicKey; 50 | } 51 | export interface SubscriptionStatus { 52 | status: 'active' | 'expired' | 'expiring_soon'; 53 | subscription: SubscriptionAccount; 54 | } 55 | export declare class SolanaAdapter { 56 | program: Program; 57 | provider: AnchorProvider; 58 | constructor(provider: AnchorProvider); 59 | setDataProviderFee(params: SetDataProviderFeeParams): Promise; 60 | createSubscription(params: CreateSubscriptionParams): Promise; 61 | getSubscriptionStatus(endTime: BN): 'active' | 'expired' | 'expiring_soon'; 62 | getProviderTokenAccount(providerAddress: PublicKey): Promise; 63 | getSubscriptionsForProvider(providerPublicKey: PublicKey): Promise; 64 | getAllSubscriptionsForUser(userPublicKey: PublicKey): Promise; 65 | renewSubscription(params: RenewParams): Promise; 66 | cancelSubscription(params: CancelParams): Promise; 67 | getSubscriptionState(subscriptionPDA: PublicKey): Promise; 68 | getQualityInfo(dataProvider: PublicKey): Promise; 69 | getProgramAddresses(dataProvider: PublicKey, subscriber: PublicKey): { 70 | statePDA: PublicKey; 71 | qualityPDA: PublicKey; 72 | subscriptionPDA: PublicKey; 73 | subscribersListPDA: PublicKey; 74 | dataProviderFeePDA: PublicKey; 75 | }; 76 | private handleError; 77 | } 78 | export {}; 79 | -------------------------------------------------------------------------------- /examples/ag2-aag/web/web_server.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, WebSocket 2 | from fastapi.staticfiles import StaticFiles 3 | from fastapi.responses import FileResponse 4 | import uvicorn 5 | import json 6 | from typing import Set 7 | import queue 8 | import asyncio 9 | import os 10 | from pathlib import Path 11 | 12 | app = FastAPI() 13 | 14 | # Get the absolute path to the directories 15 | BASE_DIR = Path(__file__).resolve().parent.parent 16 | DIST_DIR = BASE_DIR / 'agent-viz' / 'dist' 17 | ASSETS_DIR = DIST_DIR / 'assets' 18 | INDEX_HTML = DIST_DIR / 'index.html' 19 | 20 | # Mount the Vite build directory for assets 21 | app.mount("/assets", StaticFiles(directory=str(ASSETS_DIR)), name="assets") 22 | 23 | active_connections: Set[WebSocket] = set() 24 | message_queue = queue.Queue() 25 | message_history = [] 26 | MAX_HISTORY = 10 27 | 28 | @app.get("/{full_path:path}") 29 | async def serve_spa(full_path: str): 30 | """Serve the SPA for all paths.""" 31 | return FileResponse(str(INDEX_HTML)) 32 | 33 | @app.websocket("/ws") 34 | async def websocket_endpoint(websocket: WebSocket): 35 | await websocket.accept() 36 | active_connections.add(websocket) 37 | 38 | if message_history: 39 | try: 40 | await websocket.send_text(json.dumps({ 41 | "current": message_history[-1], 42 | "history": message_history 43 | })) 44 | except Exception as e: 45 | print(f"Error sending history: {e}") 46 | 47 | try: 48 | while True: 49 | await websocket.receive_text() 50 | except Exception as e: 51 | print(f"WebSocket error: {e}") 52 | active_connections.remove(websocket) 53 | 54 | async def broadcast_messages(): 55 | while True: 56 | if not message_queue.empty(): 57 | message = message_queue.get() 58 | if len(message_history) >= MAX_HISTORY: 59 | message_history.pop(0) 60 | message_history.append(message) 61 | 62 | for connection in list(active_connections): 63 | try: 64 | await connection.send_text(json.dumps({ 65 | "current": message, 66 | "history": message_history 67 | })) 68 | except Exception as e: 69 | print(f"Error in broadcast: {e}") 70 | active_connections.remove(connection) 71 | await asyncio.sleep(0.1) 72 | 73 | @app.on_event("startup") 74 | async def startup_event(): 75 | asyncio.create_task(broadcast_messages()) 76 | 77 | class WebVisualizer: 78 | def __init__(self, host="localhost", port=8000): 79 | self.host = host 80 | self.port = port 81 | self._server_thread = None 82 | 83 | def update_agent_status(self, agent_name: str, message: str, thinking: bool = False): 84 | """Update agent status and add to message queue.""" 85 | print(f"Agent {agent_name}: {message} ({'thinking' if thinking else 'idle'})") 86 | message_queue.put({ 87 | "agent": agent_name, 88 | "status": { 89 | "message": message, 90 | "isProcessing": thinking 91 | } 92 | }) 93 | 94 | def start(self): 95 | """Start the web server in the main thread.""" 96 | print(f"\nStarting visualization server at http://{self.host}:{self.port}") 97 | print("Open this URL in your browser to see the agent visualization") 98 | uvicorn.run(app, host=self.host, port=self.port, log_level="error") -------------------------------------------------------------------------------- /examples/ag2-aag/README.md: -------------------------------------------------------------------------------- 1 | # Multi-Agent System with FXN Agent Discovery 2 | 3 | This project implements a multi-agent system that includes a reasoning agent, FXN discovery agent, and user proxy agent. It has a visualizer to display interactions between agents 4 | 5 | ## Project Structure 6 | 7 | ``` 8 | your_project_root/ 9 | ├── web/ 10 | │ ├── templates/ 11 | │ ├── static/ 12 | │ └── web_server.py 13 | ├── agent_network.py 14 | ├── agent_visualizer.py 15 | ├── enhanced_reasoning_agent.py 16 | └── requirements.txt 17 | ``` 18 | 19 | ## Prerequisites 20 | 21 | - Python 3.8 or higher 22 | - pip (Python package installer) 23 | - OpenAI API key 24 | 25 | ## Installation 26 | 27 | 1. Clone the repository: 28 | ```bash 29 | git clone https://github.com/Oz-Networks/fxn-protocol-sdk 30 | cd fxn-protocol-sdk/examples/ag2-aag 31 | ``` 32 | 33 | 2. Create and activate a virtual environment (recommended): 34 | ```bash 35 | # On macOS/Linux 36 | python -m venv venv 37 | source venv/bin/activate 38 | 39 | # On Windows 40 | python -m venv venv 41 | .\venv\Scripts\activate 42 | ``` 43 | 44 | 3. Install the required packages: 45 | ```bash 46 | pip install -r requirements.txt 47 | ``` 48 | 49 | 4. Create a `.env` file in the project root and add your OpenAI API key: 50 | ``` 51 | OPENAI_API_KEY=your_api_key_here 52 | ``` 53 | 54 | ## Project Setup 55 | 56 | 1. Build the visualizer (optional - for local testing). Node >=23 recommended. 57 | ```bash 58 | cd ./agent-viz 59 | npm install 60 | npm run build 61 | ``` 62 | 63 | 2. Create the necessary files with the provided code: 64 | - `web/web_server.py`: Contains the web visualization server 65 | - `agent_network.py`: Implements the expert finder agent 66 | - `enhanced_reasoning_agent.py`: Main agent orchestration 67 | - `requirements.txt`: Project dependencies 68 | 69 | ## Running the Application 70 | 71 | 1. Start the application: 72 | ```bash 73 | python enhanced_reasoning_agent.py 74 | ``` 75 | 76 | 2. Open your web browser and navigate to: 77 | ``` 78 | http://localhost:8000 79 | ``` 80 | 81 | 3. You should see the agent visualization interface showing three agents: 82 | - User Proxy 83 | - Reasoning Agent 84 | - FXN (Expert Finder) 85 | 86 | The visualization will update in real-time as the agents process information and interact with each other. 87 | 88 | ## Features 89 | 90 | - Real-time visualization of agent states 91 | - Web-based interface using React 92 | - WebSocket communication for live updates 93 | - SVG-based agent representations 94 | - Processing state indicators 95 | - Configurable processing delays for better visualization 96 | 97 | ## Development 98 | 99 | To modify the visualization: 100 | 1. Edit the React component in `web/web_server.py` 101 | 2. Adjust the processing delay in `enhanced_reasoning_agent.py` by modifying `self.VISUALIZATION_DELAY` 102 | 3. Modify agent behaviors in their respective files 103 | 104 | ## Troubleshooting 105 | 106 | 1. If you see "No module named 'xxx'" errors: 107 | - Ensure you've activated your virtual environment 108 | - Run `pip install -r requirements.txt` again 109 | 110 | 2. If the visualization doesn't update: 111 | - Check the browser console for WebSocket errors 112 | - Ensure no other service is using port 8000 113 | - Verify your browser supports WebSocket connections 114 | 115 | 3. If agents stop responding: 116 | - Check your OpenAI API key is valid 117 | - Verify your internet connection 118 | - Check the Python console for error messages 119 | 120 | ## License 121 | 122 | MIT 123 | 124 | ## Contributing 125 | 126 | Feel free to submit a PR enhancing or extending any example in the sdk -------------------------------------------------------------------------------- /dist/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.getConfig = exports.initializeConfig = exports.ConfigurationManager = void 0; 4 | const web3_js_1 = require("@solana/web3.js"); 5 | class AddressValidator { 6 | static validate(address, label) { 7 | try { 8 | new web3_js_1.PublicKey(address); 9 | } 10 | catch (error) { 11 | throw new Error(`Invalid ${label}: ${address}`); 12 | } 13 | } 14 | } 15 | const NETWORK_CONFIGS = { 16 | mainnet: { 17 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 18 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 19 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 20 | rpcEndpoint: 'https://api.mainnet-beta.solana.com', 21 | wsEndpoint: 'wss://api.mainnet-beta.solana.com' 22 | }, 23 | testnet: { 24 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 25 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 26 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 27 | rpcEndpoint: 'https://api.testnet.solana.com', 28 | wsEndpoint: 'wss://api.testnet.solana.com' 29 | }, 30 | devnet: { 31 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 32 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 33 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 34 | rpcEndpoint: 'https://api.devnet.solana.com', 35 | wsEndpoint: 'wss://api.devnet.solana.com' 36 | } 37 | }; 38 | class ConfigurationManager { 39 | constructor(config) { 40 | this.currentConfig = Object.assign(Object.assign({}, config), { timeout: config.timeout || 30000, commitment: config.commitment || 'confirmed' }); 41 | this.networkConfig = NETWORK_CONFIGS[config.network]; 42 | this.validateAddresses(); 43 | } 44 | static initialize(config) { 45 | if (!ConfigurationManager.instance) { 46 | ConfigurationManager.instance = new ConfigurationManager(config); 47 | } 48 | return ConfigurationManager.instance; 49 | } 50 | static getInstance() { 51 | if (!ConfigurationManager.instance) { 52 | throw new Error('ConfigurationManager must be initialized before use'); 53 | } 54 | return ConfigurationManager.instance; 55 | } 56 | validateAddresses() { 57 | AddressValidator.validate(this.networkConfig.subscriptionManagerAddress, 'subscription manager address'); 58 | AddressValidator.validate(this.networkConfig.nftTokenAddress, 'NFT token address'); 59 | AddressValidator.validate(this.networkConfig.fxnMintAddress, 'FXN mint address'); 60 | } 61 | getConfig() { 62 | return Object.assign({}, this.currentConfig); 63 | } 64 | getNetworkConfig() { 65 | return Object.assign({}, this.networkConfig); 66 | } 67 | updateConfig(config) { 68 | if (config.network && config.network !== this.currentConfig.network) { 69 | this.networkConfig = NETWORK_CONFIGS[config.network]; 70 | this.validateAddresses(); 71 | } 72 | this.currentConfig = Object.assign(Object.assign({}, this.currentConfig), config); 73 | } 74 | } 75 | exports.ConfigurationManager = ConfigurationManager; 76 | // Export singleton instance creator 77 | const initializeConfig = (config) => { 78 | return ConfigurationManager.initialize(config); 79 | }; 80 | exports.initializeConfig = initializeConfig; 81 | // Export config getter 82 | const getConfig = () => { 83 | return ConfigurationManager.getInstance(); 84 | }; 85 | exports.getConfig = getConfig; 86 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/fxn-ag2/README.md: -------------------------------------------------------------------------------- 1 | # FXN Protocol + AG2 Integration Example 2 | 3 | This repository demonstrates an integration between the [FXN Protocol](https://fxn.world) and [AG2](https://docs.ag2.ai/docs/Home) (Formerly AutoGen), showcasing how to create an AI agent swarm that processes receipts for subscribed users. 4 | 5 | ## Overview 6 | 7 | This example implements a bookkeeping swarm that: 8 | 1. Connects to the FXN Protocol to manage subscriptions 9 | 2. Uses AG2's multi-agent system to process receipt images 10 | 3. Demonstrates provider-subscriber communication patterns 11 | 4. Shows how to handle async workflows with Python and TypeScript 12 | 13 | ### Components 14 | 15 | - **FXN Protocol SDK**: Handles subscription management and provider-subscriber communication 16 | - **AG2 Agents**: 17 | - `image_analyzer`: Processes receipt images using GPT-4V 18 | - `data_processor`: Standardizes and categorizes receipt data 19 | - `spreadsheet_manager`: Prepares data for storage 20 | 21 | ## Prerequisites 22 | 23 | - Python 3.10+ 24 | - Node.js 22+ 25 | - Docker and Docker Compose (optional) 26 | 27 | ## Installation 28 | 29 | ### Using Docker (Recommended) 30 | 31 | 1. Clone the repository: 32 | ```bash 33 | git clone https://github.com/yourusername/fxn-ag2.git 34 | cd fxn-ag2 35 | ``` 36 | 37 | 2. Create a `.env` file: 38 | ```env 39 | OPENAI_API_KEY=your_openai_api_key 40 | SOLANA_PRIVATE_KEY=your_solana_private_key 41 | FXN_SDK_PORT=3000 42 | AUTOGEN_USE_DOCKER=False 43 | ``` 44 | 45 | 3. Run with Docker: 46 | ```bash 47 | chmod +x run.sh 48 | ./run.sh 49 | ``` 50 | 51 | ### Local Development Setup 52 | 53 | 1. Install Python dependencies: 54 | ```bash 55 | python -m venv venv 56 | source venv/bin/activate # On Windows: venv\Scripts\activate 57 | pip install -r requirements.txt 58 | ``` 59 | 60 | 2. Install FXN SDK dependencies: 61 | ```bash 62 | cd fxn-protocol-sdk 63 | npm install 64 | npm run build:server 65 | ``` 66 | 67 | 3. Run the services: 68 | 69 | Terminal 1 (FXN SDK Server): 70 | ```bash 71 | cd fxn-protocol-sdk 72 | npm run start 73 | ``` 74 | 75 | Terminal 2 (Bookkeeping Swarm): 76 | ```bash 77 | source venv/bin/activate 78 | python -m src.main 79 | ``` 80 | 81 | ## Development 82 | 83 | ### Running Tests 84 | 85 | Install development dependencies: 86 | ```bash 87 | pip install -r requirements-dev.txt 88 | ``` 89 | 90 | Run tests: 91 | ```bash 92 | pytest -v tests/ --log-cli-level=DEBUG 93 | ``` 94 | 95 | ## How It Works 96 | 97 | 1. The swarm runs as a FXN Protocol provider, polling for active subscribers 98 | 2. When a subscriber is found, the swarm makes an offer to process receipts 99 | 3. Subscribers can respond with receipt processing requests 100 | 4. The AG2 agent swarm processes the receipt: 101 | - Analyzes the receipt image using GPT-4V 102 | - Extracts and standardizes the data 103 | - Prepares the data for storage 104 | 5. Results are sent back to the subscriber 105 | 106 | ## Architecture 107 | 108 | ``` 109 | ┌─────────────────┐ ┌──────────────┐ ┌────────────────┐ 110 | │ FXN Protocol │←────│ Bookkeeping │────→│ AG2 Agents │ 111 | │ Server │ │ Swarm │ │ │ 112 | └─────────────────┘ └──────────────┘ └────────────────┘ 113 | ↑ ↓ 114 | │ │ 115 | └─────────────────────┘ 116 | Subscriber Communication 117 | ``` 118 | 119 | ## Contributing 120 | 121 | 1. Fork the repository 122 | 2. Create your feature branch 123 | 3. Commit your changes 124 | 4. Push to the branch 125 | 5. Create a new Pull Request 126 | 127 | ## License 128 | 129 | MIT license. 130 | 131 | ## Notes 132 | 133 | This is an example integration and may need additional security and error handling for production use. It demonstrates basic concepts of: 134 | - FXN Protocol subscription management 135 | - AG2 multi-agent systems 136 | - Async Python with aiohttp 137 | - TypeScript/Node.js services 138 | - Docker containerization 139 | 140 | For more information: 141 | - [FXN Protocol Documentation](https://fxn.world) 142 | - [AG2 Documentation](https://docs.ag2.ai/docs/Home) 143 | -------------------------------------------------------------------------------- /python/fxn_protocol/client.py: -------------------------------------------------------------------------------- 1 | # python/fxn_protocol/client.py 2 | import requests 3 | import subprocess 4 | import atexit 5 | import time 6 | from typing import Optional, List, Dict, Any 7 | from dataclasses import dataclass 8 | 9 | @dataclass 10 | class Subscription: 11 | end_time: int 12 | recipient: str 13 | subscription_pda: str 14 | status: str 15 | 16 | class FXNClient: 17 | def __init__(self, port: int = 3000, host: str = "localhost"): 18 | self.base_url = f"http://{host}:{port}" 19 | self._start_server(port) 20 | 21 | def _start_server(self, port: int): 22 | # Start the Node.js server 23 | self.server_process = subprocess.Popen( 24 | ["node", "server/dist/index.js"], 25 | env={"PORT": str(port)} 26 | ) 27 | # Give the server time to start 28 | time.sleep(1) 29 | atexit.register(self._stop_server) 30 | 31 | def _stop_server(self): 32 | if hasattr(self, 'server_process'): 33 | self.server_process.terminate() 34 | 35 | def subscribe(self, provider: dict, data_provider: str, recipient: str, 36 | duration_in_days: int, nft_token_account: str) -> str: 37 | """Create a new subscription""" 38 | response = requests.post(f"{self.base_url}/subscribe", json={ 39 | "provider": provider, 40 | "dataProvider": data_provider, 41 | "recipient": recipient, 42 | "durationInDays": duration_in_days, 43 | "nftTokenAccount": nft_token_account 44 | }) 45 | response.raise_for_status() 46 | return response.json()["signature"] 47 | 48 | def renew(self, provider: dict, data_provider: str, new_recipient: str, 49 | new_end_time: int, quality_score: int, nft_token_account: str) -> str: 50 | """Renew an existing subscription""" 51 | response = requests.post(f"{self.base_url}/renew", json={ 52 | "provider": provider, 53 | "dataProvider": data_provider, 54 | "newRecipient": new_recipient, 55 | "newEndTime": new_end_time, 56 | "qualityScore": quality_score, 57 | "nftTokenAccount": nft_token_account 58 | }) 59 | response.raise_for_status() 60 | return response.json()["signature"] 61 | 62 | def cancel(self, provider: dict, data_provider: str, quality_score: int, 63 | nft_token_account: Optional[str] = None) -> str: 64 | """Cancel a subscription""" 65 | response = requests.post(f"{self.base_url}/cancel", json={ 66 | "provider": provider, 67 | "dataProvider": data_provider, 68 | "qualityScore": quality_score, 69 | "nftTokenAccount": nft_token_account 70 | }) 71 | response.raise_for_status() 72 | return response.json()["signature"] 73 | 74 | def get_provider_subscriptions(self, provider: dict, provider_address: str) -> List[Subscription]: 75 | """Get all subscriptions for a provider""" 76 | response = requests.get( 77 | f"{self.base_url}/subscriptions/provider/{provider_address}", 78 | json={"provider": provider} 79 | ) 80 | response.raise_for_status() 81 | return [Subscription(**sub) for sub in response.json()["subscriptions"]] 82 | 83 | def get_user_subscriptions(self, provider: dict, user_address: str) -> List[Subscription]: 84 | """Get all subscriptions for a user""" 85 | response = requests.get( 86 | f"{self.base_url}/subscriptions/user/{user_address}", 87 | json={"provider": provider} 88 | ) 89 | response.raise_for_status() 90 | return [Subscription(**sub) for sub in response.json()["subscriptions"]] 91 | 92 | def set_fee(self, provider: dict, fee: int) -> str: 93 | """Set the data provider fee""" 94 | response = requests.post(f"{self.base_url}/fee", json={ 95 | "provider": provider, 96 | "fee": fee 97 | }) 98 | response.raise_for_status() 99 | return response.json()["signature"] 100 | -------------------------------------------------------------------------------- /examples/ag2-aag/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | .idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | export type NetworkType = 'mainnet' | 'testnet' | 'devnet'; 4 | 5 | export interface NetworkConfig { 6 | subscriptionManagerAddress: string; 7 | nftTokenAddress: string; 8 | fxnMintAddress: string; 9 | rpcEndpoint: string; 10 | wsEndpoint?: string; 11 | } 12 | 13 | export interface SDKConfig { 14 | network: NetworkType; 15 | timeout?: number; 16 | commitment?: 'processed' | 'confirmed' | 'finalized'; 17 | } 18 | 19 | class AddressValidator { 20 | static validate(address: string, label: string): void { 21 | try { 22 | new PublicKey(address); 23 | } catch (error) { 24 | throw new Error(`Invalid ${label}: ${address}`); 25 | } 26 | } 27 | } 28 | 29 | const NETWORK_CONFIGS: Record = { 30 | mainnet: { 31 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 32 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 33 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 34 | rpcEndpoint: 'https://api.mainnet-beta.solana.com', 35 | wsEndpoint: 'wss://api.mainnet-beta.solana.com' 36 | }, 37 | testnet: { 38 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 39 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 40 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 41 | rpcEndpoint: 'https://api.testnet.solana.com', 42 | wsEndpoint: 'wss://api.testnet.solana.com' 43 | }, 44 | devnet: { 45 | subscriptionManagerAddress: 'AnPhQYFcJEPBG2JTrvaNne85rXufC1Q97bu29YaWvKDs', 46 | nftTokenAddress: '3sH789kj7yAtmuJKJQqKnxdWd9Q28qfN1DzkeFZd7ty7', 47 | fxnMintAddress: '34dcPojKodMA2GkH2E9jjNi3gheweipGDaUAgoX73dK8', 48 | rpcEndpoint: 'https://api.devnet.solana.com', 49 | wsEndpoint: 'wss://api.devnet.solana.com' 50 | } 51 | }; 52 | 53 | export class ConfigurationManager { 54 | private static instance: ConfigurationManager; 55 | private currentConfig: SDKConfig; 56 | private networkConfig: NetworkConfig; 57 | 58 | private constructor(config: SDKConfig) { 59 | this.currentConfig = { 60 | ...config, 61 | timeout: config.timeout || 30000, 62 | commitment: config.commitment || 'confirmed' 63 | }; 64 | this.networkConfig = NETWORK_CONFIGS[config.network]; 65 | this.validateAddresses(); 66 | } 67 | 68 | public static initialize(config: SDKConfig): ConfigurationManager { 69 | if (!ConfigurationManager.instance) { 70 | ConfigurationManager.instance = new ConfigurationManager(config); 71 | } 72 | return ConfigurationManager.instance; 73 | } 74 | 75 | public static getInstance(): ConfigurationManager { 76 | if (!ConfigurationManager.instance) { 77 | throw new Error('ConfigurationManager must be initialized before use'); 78 | } 79 | return ConfigurationManager.instance; 80 | } 81 | 82 | private validateAddresses(): void { 83 | AddressValidator.validate(this.networkConfig.subscriptionManagerAddress, 'subscription manager address'); 84 | AddressValidator.validate(this.networkConfig.nftTokenAddress, 'NFT token address'); 85 | AddressValidator.validate(this.networkConfig.fxnMintAddress, 'FXN mint address'); 86 | } 87 | 88 | public getConfig(): SDKConfig { 89 | return { ...this.currentConfig }; 90 | } 91 | 92 | public getNetworkConfig(): NetworkConfig { 93 | return { ...this.networkConfig }; 94 | } 95 | 96 | public updateConfig(config: Partial): void { 97 | if (config.network && config.network !== this.currentConfig.network) { 98 | this.networkConfig = NETWORK_CONFIGS[config.network]; 99 | this.validateAddresses(); 100 | } 101 | this.currentConfig = { 102 | ...this.currentConfig, 103 | ...config 104 | }; 105 | } 106 | } 107 | 108 | // Export singleton instance creator 109 | export const initializeConfig = (config: SDKConfig): ConfigurationManager => { 110 | return ConfigurationManager.initialize(config); 111 | }; 112 | 113 | // Export config getter 114 | export const getConfig = (): ConfigurationManager => { 115 | return ConfigurationManager.getInstance(); 116 | }; 117 | -------------------------------------------------------------------------------- /src/types/subscription_manager.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@types/subscription_manager' { 2 | import { BN } from '@coral-xyz/anchor'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | 5 | // Event Types 6 | export interface CollectorFeeUpdatedEvent { 7 | newCollectorFee: BN; 8 | } 9 | 10 | export interface FeePerDayUpdatedEvent { 11 | newFeePerDay: BN; 12 | } 13 | 14 | export interface QualityProvidedEvent { 15 | dataProvider: PublicKey; 16 | subscriber: PublicKey; 17 | quality: number; 18 | } 19 | 20 | export interface SubscriptionCancelledEvent { 21 | dataProvider: PublicKey; 22 | subscriber: PublicKey; 23 | } 24 | 25 | export interface SubscriptionCreatedEvent { 26 | dataProvider: PublicKey; 27 | subscriber: PublicKey; 28 | recipient: string; 29 | endTime: BN; 30 | timestamp: BN; 31 | } 32 | 33 | export interface SubscriptionEndedEvent { 34 | dataProvider: PublicKey; 35 | subscriber: PublicKey; 36 | } 37 | 38 | export interface SubscriptionRenewedEvent { 39 | dataProvider: PublicKey; 40 | subscriber: PublicKey; 41 | newRecipient: string; 42 | newEndTime: BN; 43 | timestamp: BN; 44 | } 45 | 46 | // Account Types 47 | export interface QualityRecord { 48 | provider: PublicKey; 49 | quality: number; 50 | } 51 | 52 | export interface QualityInfo { 53 | subscriber: PublicKey; 54 | quality: number; 55 | currentIndex: number; 56 | qualities: QualityRecord[]; 57 | } 58 | 59 | export interface State { 60 | owner: PublicKey; 61 | nftProgramId: PublicKey; 62 | feePerDay: BN; 63 | collectorFee: BN; 64 | } 65 | 66 | export interface SubscribersList { 67 | subscribers: PublicKey[]; 68 | } 69 | 70 | export interface Subscription { 71 | endTime: BN; 72 | recipient: string; 73 | } 74 | 75 | // Program Type 76 | export interface SubscriptionManager { 77 | address: string; 78 | metadata: { 79 | name: string; 80 | version: string; 81 | spec: string; 82 | description: string; 83 | }; 84 | instructions: { 85 | cancelSubscription: (quality: number) => Promise; 86 | endSubscription: (quality: number) => Promise; 87 | getSubscribers: () => Promise; 88 | initialize: () => Promise; 89 | initializeQualityInfo: () => Promise; 90 | renewSubscription: ( 91 | newRecipient: string, 92 | newEndTime: BN, 93 | quality: number 94 | ) => Promise; 95 | setCollectorFee: (newFee: BN) => Promise; 96 | setFeePerDay: (newFee: BN) => Promise; 97 | storeDataQuality: (quality: number) => Promise; 98 | subscribe: ( 99 | recipient: string, 100 | endTime: BN 101 | ) => Promise; 102 | }; 103 | accounts: { 104 | qualityInfo: QualityInfo; 105 | state: State; 106 | subscribersList: SubscribersList; 107 | subscription: Subscription; 108 | }; 109 | errors: { 110 | periodTooShort: { code: 6000; msg: string }; 111 | alreadySubscribed: { code: 6001; msg: string }; 112 | insufficientPayment: { code: 6002; msg: string }; 113 | invalidNftHolder: { code: 6003; msg: string }; 114 | subscriptionNotFound: { code: 6004; msg: string }; 115 | qualityOutOfRange: { code: 6005; msg: string }; 116 | subscriptionAlreadyEnded: { code: 6006; msg: string }; 117 | activeSubscription: { code: 6007; msg: string }; 118 | notOwner: { code: 6008; msg: string }; 119 | }; 120 | } 121 | 122 | // Helper Types 123 | export interface ProgramAddresses { 124 | statePDA: PublicKey; 125 | qualityPDA: PublicKey; 126 | subscriptionPDA: PublicKey; 127 | subscribersListPDA: PublicKey; 128 | } 129 | 130 | export interface SubscriptionStatus { 131 | status: 'active' | 'expired' | 'expiring_soon'; 132 | subscription: Subscription; 133 | subscriptionPDA: PublicKey; 134 | dataProvider: PublicKey; 135 | } 136 | 137 | export interface SubscriberDetails { 138 | subscriber: PublicKey; 139 | subscriptionPDA: PublicKey; 140 | subscription: Subscription; 141 | status: 'active' | 'expired' | 'expiring_soon'; 142 | } 143 | 144 | export interface CreateSubscriptionParams { 145 | dataProvider: PublicKey; 146 | recipient: string; 147 | durationInDays: number; 148 | nftTokenAccount: PublicKey; 149 | } 150 | 151 | export interface RenewParams { 152 | dataProvider: PublicKey; 153 | newRecipient: string; 154 | newEndTime: number; 155 | qualityScore: number; 156 | nftTokenAccount: PublicKey; 157 | } 158 | 159 | export interface CancelParams { 160 | dataProvider: PublicKey; 161 | qualityScore: number; 162 | nftTokenAccount?: PublicKey; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent_network.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | from urllib.parse import urlencode 5 | 6 | class ExpertFinderAgent: 7 | """Agent responsible for finding and consulting domain experts.""" 8 | 9 | def __init__(self, base_url="https://fxn.world/api"): 10 | self.base_url = base_url 11 | self.name = "expert_finder" 12 | 13 | def query_experts(self, topic, page_size=24): 14 | """Query the API for experts on a specific topic.""" 15 | endpoint = f"{self.base_url}/agents" 16 | 17 | # Simplified query - just pagination and sorting 18 | params = { 19 | "pageSize": page_size, 20 | "sort": json.dumps({ 21 | "field": "createdAt", 22 | "direction": "desc" 23 | }) 24 | } 25 | 26 | try: 27 | # Make the API request 28 | response = requests.get( 29 | f"{endpoint}?{urlencode(params)}", 30 | headers={"Content-Type": "application/json"} 31 | ) 32 | 33 | # Add debug logging 34 | print(f"API Request URL: {response.url}") 35 | print(f"Response status: {response.status_code}") 36 | if not response.ok: 37 | print(f"Error response: {response.text}") 38 | 39 | response.raise_for_status() 40 | return response.json() 41 | 42 | except requests.exceptions.RequestException as e: 43 | print(f"Error querying experts: {e}") 44 | return None 45 | 46 | def format_expert_info(self, expert): 47 | """Format expert information for agent consumption.""" 48 | return { 49 | 'name': expert.get('name', ''), 50 | 'description': expert.get('description', ''), 51 | 'fee_per_day': expert.get('feePerDay', 0), 52 | 'status': expert.get('status', '') 53 | } 54 | 55 | def find_relevant_expert(self, query): 56 | """Find the most relevant expert for a given query.""" 57 | try: 58 | experts_response = self.query_experts(None) 59 | if experts_response and 'agents' in experts_response: 60 | experts = experts_response['agents'] 61 | if experts: 62 | # Filter for active agents only 63 | active_experts = [e for e in experts if e.get('status') == 'active'] 64 | 65 | # Score each expert based on description matching 66 | query_terms = set(self._extract_topics(query)) 67 | scored_experts = [] 68 | 69 | for expert in active_experts: 70 | description = expert.get('description', '').lower() 71 | if not description: 72 | continue 73 | 74 | # Calculate relevance score based on term matches in description 75 | description_terms = set(self._extract_topics(description)) 76 | score = len(description_terms.intersection(query_terms)) 77 | 78 | if score > 0: 79 | scored_experts.append((score, expert)) 80 | 81 | # Return the best match if any 82 | if scored_experts: 83 | scored_experts.sort(reverse=True) # Sort by score 84 | return self.format_expert_info(scored_experts[0][1]) 85 | 86 | return None 87 | except Exception as e: 88 | print(f"Error finding relevant expert: {e}") 89 | return None 90 | 91 | def _extract_topics(self, text): 92 | """Extract potential topics from text.""" 93 | if not text: 94 | return [] 95 | 96 | # Remove common words and split into potential topics 97 | common_words = {'the', 'a', 'an', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'and', 98 | 'this', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 99 | 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'test', 'part'} 100 | 101 | words = text.lower().split() 102 | topics = [ 103 | word for word in words 104 | if word not in common_words 105 | and len(word) > 2 # Reduced minimum length to catch more meaningful terms 106 | and not any(char in word for char in '.,?!:;()') 107 | ] 108 | print(f"Extracted topics from text: {topics}") 109 | return topics 110 | 111 | def receive(self, message, sender): 112 | """Handle incoming messages from other agents.""" 113 | if "need_expert" in message.lower(): 114 | # Clean the message by removing the 'need_expert:' prefix 115 | query = message.lower().replace('need_expert:', '').strip() 116 | expert = self.find_relevant_expert(query) 117 | if expert: 118 | return f"Found relevant expert: {json.dumps(expert, indent=2)}" 119 | return "No relevant experts found." 120 | return "I can help find experts. Please include 'need_expert' in your request." -------------------------------------------------------------------------------- /dist/server/server/src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | // server/src/index.ts 16 | const express_1 = __importDefault(require("express")); 17 | const fxn_solana_adapter_1 = require("../../src/client/fxn-solana-adapter"); 18 | const anchor_1 = require("@coral-xyz/anchor"); 19 | const web3_js_1 = require("@solana/web3.js"); 20 | const app = (0, express_1.default)(); 21 | app.use(express_1.default.json()); 22 | // Helper to create SolanaAdapter instance from request 23 | const getAdapter = (provider) => { 24 | return new fxn_solana_adapter_1.SolanaAdapter(new anchor_1.AnchorProvider(provider.connection, provider.wallet, provider.opts)); 25 | }; 26 | // Subscribe endpoint 27 | app.post('/subscribe', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 28 | try { 29 | const { provider, dataProvider, recipient, durationInDays, nftTokenAccount } = req.body; 30 | const adapter = getAdapter(provider); 31 | const result = yield adapter.createSubscription({ 32 | dataProvider: new web3_js_1.PublicKey(dataProvider), 33 | recipient, 34 | durationInDays, 35 | nftTokenAccount: new web3_js_1.PublicKey(nftTokenAccount) 36 | }); 37 | res.json({ success: true, signature: result }); 38 | } 39 | catch (error) { 40 | res.status(500).json({ success: false, error: error.message }); 41 | } 42 | })); 43 | // Renew endpoint 44 | app.post('/renew', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 45 | try { 46 | const { provider, dataProvider, newRecipient, newEndTime, qualityScore, nftTokenAccount } = req.body; 47 | const adapter = getAdapter(provider); 48 | const result = yield adapter.renewSubscription({ 49 | dataProvider: new web3_js_1.PublicKey(dataProvider), 50 | newRecipient, 51 | newEndTime, 52 | qualityScore, 53 | nftTokenAccount: new web3_js_1.PublicKey(nftTokenAccount) 54 | }); 55 | res.json({ success: true, signature: result }); 56 | } 57 | catch (error) { 58 | res.status(500).json({ success: false, error: error.message }); 59 | } 60 | })); 61 | // Cancel endpoint 62 | app.post('/cancel', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 63 | try { 64 | const { provider, dataProvider, qualityScore, nftTokenAccount } = req.body; 65 | const adapter = getAdapter(provider); 66 | const result = yield adapter.cancelSubscription({ 67 | dataProvider: new web3_js_1.PublicKey(dataProvider), 68 | qualityScore, 69 | nftTokenAccount: nftTokenAccount ? new web3_js_1.PublicKey(nftTokenAccount) : undefined 70 | }); 71 | res.json({ success: true, signature: result }); 72 | } 73 | catch (error) { 74 | res.status(500).json({ success: false, error: error.message }); 75 | } 76 | })); 77 | // Get subscriptions for provider 78 | app.get('/subscriptions/provider/:providerAddress', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 79 | try { 80 | const { provider } = req.body; 81 | const { providerAddress } = req.params; 82 | const adapter = getAdapter(provider); 83 | const result = yield adapter.getSubscriptionsForProvider(new web3_js_1.PublicKey(providerAddress)); 84 | res.json({ success: true, subscriptions: result }); 85 | } 86 | catch (error) { 87 | res.status(500).json({ success: false, error: error.message }); 88 | } 89 | })); 90 | // Get subscriptions for user 91 | app.get('/subscriptions/user/:userAddress', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 92 | try { 93 | const { provider } = req.body; 94 | const { userAddress } = req.params; 95 | const adapter = getAdapter(provider); 96 | const result = yield adapter.getAllSubscriptionsForUser(new web3_js_1.PublicKey(userAddress)); 97 | res.json({ success: true, subscriptions: result }); 98 | } 99 | catch (error) { 100 | res.status(500).json({ success: false, error: error.message }); 101 | } 102 | })); 103 | // Set fee 104 | app.post('/fee', (req, res) => __awaiter(void 0, void 0, void 0, function* () { 105 | try { 106 | const { provider, fee } = req.body; 107 | const adapter = getAdapter(provider); 108 | const result = yield adapter.setDataProviderFee({ fee }); 109 | res.json({ success: true, signature: result }); 110 | } 111 | catch (error) { 112 | res.status(500).json({ success: false, error: error.message }); 113 | } 114 | })); 115 | const PORT = process.env.PORT || 3000; 116 | app.listen(PORT, () => { 117 | console.log(`Server running on port ${PORT}`); 118 | }); 119 | -------------------------------------------------------------------------------- /dist/client/fxn-solana-adapter.d.ts: -------------------------------------------------------------------------------- 1 | import { Program, AnchorProvider, IdlAccounts, BN } from '@coral-xyz/anchor'; 2 | import { PublicKey, TransactionSignature } from '@solana/web3.js'; 3 | import type { SubscriptionManager } from '@/types/subscription_manager'; 4 | export interface RenewParams { 5 | dataProvider: PublicKey; 6 | newRecipient: string; 7 | newEndTime: number; 8 | qualityScore: number; 9 | } 10 | export interface CancelParams { 11 | dataProvider: PublicKey; 12 | qualityScore: number; 13 | } 14 | export interface SubscriptionState { 15 | endTime: BN; 16 | recipient: string; 17 | } 18 | export interface SubscriberDetails { 19 | subscriber: PublicKey; 20 | subscriptionPDA: PublicKey; 21 | subscription: { 22 | endTime: BN; 23 | recipient: string; 24 | }; 25 | status: 'active' | 'expired' | 'expiring_soon'; 26 | } 27 | export interface SubscriptionDetails { 28 | dataProvider: PublicKey; 29 | subscription: PublicKey; 30 | endTime: BN; 31 | recipient: string; 32 | } 33 | export interface SetDataProviderFeeParams { 34 | fee: number; 35 | } 36 | type QualityInfoAccount = IdlAccounts['qualityInfo']; 37 | type SubscriptionAccount = IdlAccounts['subscription']; 38 | export declare enum SubscriptionErrorCode { 39 | PeriodTooShort = 6000, 40 | AlreadySubscribed = 6001, 41 | InsufficientPayment = 6002, 42 | InvalidTokenAccount = 6003, 43 | SubscriptionNotFound = 6004, 44 | QualityOutOfRange = 6005, 45 | SubscriptionAlreadyEnded = 6006, 46 | ActiveSubscription = 6007, 47 | NotOwner = 6008, 48 | TooManyRequests = 6009, 49 | NoSubscriptionRequest = 6010, 50 | RequestNotApproved = 6011, 51 | Unauthorized = 6012, 52 | InvalidDataProvider = 6013, 53 | InvalidDataProviderFeeAccount = 6014, 54 | InvalidOwnerFeeAccount = 6015, 55 | InvalidDataProviderPaymentAccount = 6016, 56 | InvalidOwnerPaymentAccount = 6017, 57 | TooManySubscriptions = 6018, 58 | TooManySubscribers = 6019, 59 | InvalidIndex = 6020, 60 | AlreadyApproved = 6021, 61 | InvalidSubscriber = 6022, 62 | AlreadyRequested = 6023 63 | } 64 | export interface CreateSubscriptionParams { 65 | dataProvider: PublicKey; 66 | recipient: string; 67 | durationInDays: number; 68 | } 69 | export interface RequestSubscriptionParams { 70 | dataProvider: PublicKey; 71 | } 72 | export interface ApproveSubscriptionRequestParams { 73 | subscriberAddress: PublicKey; 74 | requestIndex: number; 75 | } 76 | export interface SubscriptionListParams { 77 | dataProvider: PublicKey; 78 | } 79 | interface _SubscriptionListParams { 80 | subscriber: PublicKey; 81 | dataProvider: PublicKey; 82 | mySubscriptionsPDA: PublicKey; 83 | subscribersListPDA: PublicKey; 84 | } 85 | export interface AgentParams { 86 | name: string; 87 | description: string; 88 | restrictSubscriptions: boolean; 89 | capabilities: string[]; 90 | fee: number; 91 | } 92 | export interface AgentProfile { 93 | pubkey: PublicKey; 94 | name: string; 95 | description: string; 96 | restrictSubscriptions: boolean; 97 | capabilities: string[]; 98 | subscriberCount: number; 99 | fee: number; 100 | } 101 | export interface SubscriptionStatus { 102 | status: 'active' | 'expired' | 'expiring_soon'; 103 | subscription: SubscriptionAccount; 104 | } 105 | export interface RequestStruct { 106 | subscriberPubkey: PublicKey; 107 | approved: boolean; 108 | } 109 | export interface QualityInfoParams { 110 | dataProvider: PublicKey; 111 | qualityScore: number; 112 | } 113 | export declare class SolanaAdapter { 114 | program: Program; 115 | provider: AnchorProvider; 116 | constructor(provider: AnchorProvider); 117 | registerAgent(params: AgentParams): Promise; 118 | editAgentDetails(params: AgentParams): Promise; 119 | getAgentDetails(dataProvider: PublicKey): Promise; 120 | requestSubscription(params: RequestSubscriptionParams): Promise; 121 | approveSubscriptionRequest(params: ApproveSubscriptionRequestParams): Promise; 122 | getSubscriptionRequests(dataProvider: PublicKey): Promise; 123 | setDataProviderFee(params: SetDataProviderFeeParams): Promise; 124 | createSubscription(params: CreateSubscriptionParams): Promise<[TransactionSignature, TransactionSignature]>; 125 | subscriptionLists(params: SubscriptionListParams): Promise; 126 | reallocSubscriptionLists(params: _SubscriptionListParams): Promise; 127 | initMySubscriptionsList(params: _SubscriptionListParams): Promise; 128 | initSubscribersList(params: _SubscriptionListParams): Promise; 129 | addSubscriptionsLists(params: _SubscriptionListParams): Promise; 130 | getSubscriptionStatus(endTime: BN): 'active' | 'expired' | 'expiring_soon'; 131 | getSubscriptionsForProvider(providerPublicKey: PublicKey): Promise; 132 | getAllSubscriptionsForUser(userPublicKey: PublicKey): Promise; 133 | renewSubscription(params: RenewParams): Promise; 134 | cancelSubscription(params: CancelParams): Promise; 135 | getSubscriptionState(subscriptionPDA: PublicKey): Promise; 136 | getQualityInfo(dataProvider: PublicKey): Promise; 137 | storeQualityInfo(params: QualityInfoParams): Promise; 138 | getAllAgents(): Promise; 139 | getProgramAddresses(dataProvider: PublicKey, subscriber: PublicKey): { 140 | statePDA: PublicKey; 141 | qualityPDA: PublicKey; 142 | subscriptionPDA: PublicKey; 143 | subscribersListPDA: PublicKey; 144 | dataProviderFeePDA: PublicKey; 145 | mySubscriptionsPDA: PublicKey; 146 | subscriptionRequestsPDA: PublicKey; 147 | }; 148 | private handleError; 149 | } 150 | export {}; 151 | -------------------------------------------------------------------------------- /examples/ag2-aag/agent-viz/src/components/AgentVisualization.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | // SVG Robot components 5 | const BaseRobot = ({ name, isProcessing, type = 'default' }) => { 6 | console.log('rerendering base robot ', name, isProcessing); 7 | const [dots, setDots] = useState('...'); 8 | 9 | useEffect(() => { 10 | if (isProcessing) { 11 | const interval = setInterval(() => { 12 | setDots(prev => prev.length >= 3 ? '.' : prev + '.'); 13 | }, 500); 14 | return () => clearInterval(interval); 15 | } 16 | }, [isProcessing]); 17 | 18 | return ( 19 | 20 | {isProcessing && ( 21 | 22 | {dots} 23 | 24 | )} 25 | 26 | 37 | 38 | {type === 'FXN' ? ( 39 | <> 40 | 41 | 42 | 43 | ) : type === 'grader' ? ( 44 | <> 45 | 46 | 47 | 48 | ) : ( 49 | <> 50 | 51 | 52 | 53 | )} 54 | 55 | 61 | 62 | 70 | 77 | {name} 78 | 79 | 80 | ); 81 | }; 82 | 83 | const MessageFeed = ({ messages }) => { 84 | console.log('MessageFeed render:', { messages }); 85 | return ( 86 |
87 |
88 | {messages.map((msg, idx) => ( 89 |
90 | {msg.agent}: 91 | {msg.message} 92 |
93 | ))} 94 |
95 |
96 | ); 97 | } 98 | 99 | const AgentVisualization = () => { 100 | const [agents, setAgents] = useState([ 101 | { id: 1, name: 'user_proxy', type: 'default', isProcessing: false }, 102 | { id: 2, name: 'reason_agent', type: 'default', isProcessing: false }, 103 | { id: 3, name: 'tot_grader', type: 'grader', isProcessing: false }, 104 | { id: 4, name: 'FXN', type: 'FXN', isProcessing: false } 105 | ]); 106 | 107 | const [messages, setMessages] = useState([]); 108 | 109 | useEffect(() => { 110 | const ws = new WebSocket('ws://localhost:8000/ws'); 111 | console.log('Connected to socket'); 112 | 113 | ws.onmessage = (event) => { 114 | console.log('Socket message ', event); 115 | try { 116 | const data = JSON.parse(event.data); 117 | 118 | const currentData = data?.current || {}; 119 | const agentName = currentData?.agent; 120 | const status = currentData?.status || {}; 121 | 122 | console.log('status is', status); 123 | console.log('data is', currentData); 124 | console.log('name is', agentName); 125 | 126 | if (agentName) { 127 | setAgents(prev => prev.map(agent => 128 | agent.name === agentName 129 | ? {...agent, isProcessing: Boolean(status.isProcessing)} 130 | : agent 131 | )); 132 | } 133 | 134 | if (status.message) { 135 | setMessages(prev => [...prev, { 136 | agent: agentName, 137 | message: status.message 138 | }].slice(-10)); // Keep last 10 messages 139 | } 140 | } catch (error) { 141 | console.error('Error processing WebSocket message:', error); 142 | } 143 | }; 144 | 145 | ws.onerror = (error) => { 146 | console.error('WebSocket error:', error); 147 | }; 148 | 149 | ws.onclose = () => { 150 | console.log('WebSocket connection closed'); 151 | }; 152 | 153 | return () => { 154 | if (ws.readyState === WebSocket.OPEN) { 155 | ws.close(); 156 | } 157 | }; 158 | }, []); 159 | 160 | const localAgents = agents.filter(agent => agent.name !== 'FXN'); 161 | const fxnAgent = agents.find(agent => agent.name === 'FXN'); 162 | 163 | return ( 164 |
165 |
166 |
167 | 168 |

169 | Multi-Agent System with Agent Augmented Generation (AAG) 170 |

171 |
172 | 173 |
174 |
175 |
176 | {localAgents.map(agent => ( 177 |
183 | 189 |
190 |

{agent.name}

191 |

196 | {agent.isProcessing ? 'Processing...' : 'Idle'} 197 |

198 |
199 |
200 | ))} 201 |
202 |
203 | 204 | 212 | 213 | {fxnAgent && ( 214 |
215 | 221 |
222 |

{fxnAgent.name}

223 |

228 | {fxnAgent.isProcessing ? 'Processing...' : 'Idle'} 229 |

230 |
231 |
232 | )} 233 |
234 | 235 |
236 |

237 | Agent Communication Feed 238 |

239 | 240 |
241 |
242 |
243 | ); 244 | }; 245 | 246 | export default AgentVisualization; 247 | 248 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | // server/src/index.ts 2 | import express from 'express'; 3 | import { SolanaAdapter } from '../../src/client/fxn-solana-adapter'; 4 | import { AnchorProvider } from '@coral-xyz/anchor'; 5 | import {Connection, PublicKey} from '@solana/web3.js'; 6 | 7 | const app = express(); 8 | app.use(express.json()); 9 | 10 | const DEFAULT_RPC_ENDPOINT = "https://api.devnet.solana.com"; 11 | const DEFAULT_COMMITMENT = "confirmed"; 12 | 13 | // Helper to create SolanaAdapter instance from request 14 | const getAdapter = (provider: any) => { 15 | if (!provider) { 16 | throw new Error('Provider details are required'); 17 | } 18 | if (!provider.connection) { 19 | throw new Error('Provider connection details are required'); 20 | } 21 | if (!provider.wallet) { 22 | throw new Error('Provider wallet details are required'); 23 | } 24 | 25 | console.log('Creating adapter with provider:', JSON.stringify(provider, null, 2)); 26 | 27 | return new SolanaAdapter(new AnchorProvider( 28 | provider.connection, 29 | provider.wallet, 30 | provider.opts || {} 31 | )); 32 | }; 33 | 34 | const createDefaultProvider = (publicKeyStr: string) => { 35 | // Create connection 36 | const connection = new Connection(DEFAULT_RPC_ENDPOINT, DEFAULT_COMMITMENT); 37 | 38 | // Create a minimal wallet adapter 39 | const wallet = { 40 | publicKey: new PublicKey(publicKeyStr), 41 | signTransaction: async (tx: any) => tx, 42 | signAllTransactions: async (txs: any[]) => txs 43 | }; 44 | 45 | // Create and return provider 46 | return new AnchorProvider( 47 | connection, 48 | wallet, 49 | { commitment: DEFAULT_COMMITMENT } 50 | ); 51 | }; 52 | 53 | // Register Agent endpoint 54 | app.post('/agent', async (req, res) => { 55 | try { 56 | const { 57 | provider, 58 | name, 59 | description, 60 | restrict_subscriptions, 61 | capabilities, 62 | fee 63 | } = req.body; 64 | const adapter = getAdapter(provider); 65 | 66 | const result = await adapter.registerAgent({ 67 | name, 68 | description, 69 | restrict_subscriptions, 70 | capabilities, 71 | fee, 72 | }); 73 | 74 | res.json({ success: true, signature: result }); 75 | } catch (error: any) { 76 | res.status(500).json({ success: false, error: error.message }); 77 | } 78 | }); 79 | 80 | // Edit Agent endpoint 81 | app.put('/agent', async (req, res) => { 82 | try { 83 | const { 84 | provider, 85 | name, 86 | description, 87 | restrict_subscriptions, 88 | capabilities, 89 | fee 90 | } = req.body; 91 | const adapter = getAdapter(provider); 92 | 93 | const result = await adapter.editAgentDetails({ 94 | name, 95 | description, 96 | restrict_subscriptions, 97 | capabilities, 98 | fee, 99 | }); 100 | 101 | res.json({ success: true, signature: result }); 102 | } catch (error: any) { 103 | res.status(500).json({ success: false, error: error.message }); 104 | } 105 | }); 106 | 107 | // Request Subscription endpoint 108 | app.post('/subscribe/request', async (req, res) => { 109 | try { 110 | const { 111 | provider, 112 | dataProvider, 113 | } = req.body; 114 | const adapter = getAdapter(provider); 115 | 116 | const result = await adapter.requestSubscription({ 117 | dataProvider: new PublicKey(dataProvider), 118 | }); 119 | 120 | res.json({ success: true, signature: result }); 121 | } catch (error: any) { 122 | res.status(500).json({ success: false, error: error.message }); 123 | } 124 | }); 125 | 126 | // Approve Subscription endpoint 127 | app.post('/subscribe/approve', async (req, res) => { 128 | try { 129 | const { 130 | provider, 131 | subscriberAddress, 132 | requestIndex 133 | } = req.body; 134 | const adapter = getAdapter(provider); 135 | 136 | const result = await adapter.approveSubscriptionRequest({ 137 | subscriberAddress, 138 | requestIndex 139 | }); 140 | 141 | res.json({ success: true, signature: result }); 142 | } catch (error: any) { 143 | res.status(500).json({ success: false, error: error.message }); 144 | } 145 | }); 146 | 147 | // Subscribe endpoint 148 | app.post('/subscribe', async (req, res) => { 149 | try { 150 | const { provider, dataProvider, recipient, durationInDays } = req.body; 151 | const adapter = getAdapter(provider); 152 | 153 | const result = await adapter.createSubscription({ 154 | dataProvider: new PublicKey(dataProvider), 155 | recipient, 156 | durationInDays, 157 | }); 158 | 159 | res.json({ success: true, signature: result }); 160 | } catch (error: any) { 161 | res.status(500).json({ success: false, error: error.message }); 162 | } 163 | }); 164 | 165 | // Renew endpoint 166 | app.post('/renew', async (req, res) => { 167 | try { 168 | const { provider, dataProvider, newRecipient, newEndTime, qualityScore, nftTokenAccount } = req.body; 169 | const adapter = getAdapter(provider); 170 | 171 | const result = await adapter.renewSubscription({ 172 | dataProvider: new PublicKey(dataProvider), 173 | newRecipient, 174 | newEndTime, 175 | qualityScore, 176 | }); 177 | 178 | res.json({ success: true, signature: result }); 179 | } catch (error: any) { 180 | res.status(500).json({ success: false, error: error.message }); 181 | } 182 | }); 183 | 184 | // Cancel endpoint 185 | app.post('/cancel', async (req, res) => { 186 | try { 187 | const { provider, dataProvider, qualityScore, nftTokenAccount } = req.body; 188 | const adapter = getAdapter(provider); 189 | 190 | const result = await adapter.cancelSubscription({ 191 | dataProvider: new PublicKey(dataProvider), 192 | qualityScore, 193 | }); 194 | 195 | res.json({ success: true, signature: result }); 196 | } catch (error: any) { 197 | res.status(500).json({ success: false, error: error.message }); 198 | } 199 | }); 200 | 201 | // Get subscriptions for provider 202 | app.get('/subscriptions/provider/:providerAddress', async (req, res) => { 203 | console.log('Received request for provider subscriptions'); 204 | console.log('Provider address:', req.params.providerAddress); 205 | 206 | try { 207 | const { providerAddress } = req.params; 208 | 209 | // Create provider and adapter 210 | const provider = createDefaultProvider(providerAddress); 211 | const adapter = new SolanaAdapter(provider); 212 | 213 | console.log('Created adapter, fetching subscriptions...'); 214 | 215 | const result = await adapter.getSubscriptionsForProvider( 216 | new PublicKey(providerAddress) 217 | ); 218 | 219 | console.log('Got result:', result); 220 | 221 | res.json({ success: true, subscriptions: result }); 222 | } catch (error: any) { 223 | console.error('Error processing request:', error); 224 | res.status(500).json({ 225 | success: false, 226 | error: error.message || 'Internal server error', 227 | details: error.stack 228 | }); 229 | } 230 | }); 231 | 232 | // Get subscriptions for user 233 | app.get('/subscriptions/user/:userAddress', async (req, res) => { 234 | try { 235 | const { provider } = req.body; 236 | const { userAddress } = req.params; 237 | const adapter = getAdapter(provider); 238 | 239 | const result = await adapter.getAllSubscriptionsForUser( 240 | new PublicKey(userAddress) 241 | ); 242 | 243 | res.json({ success: true, subscriptions: result }); 244 | } catch (error: any) { 245 | res.status(500).json({ success: false, error: error.message }); 246 | } 247 | }); 248 | 249 | // Set fee 250 | app.post('/fee', async (req, res) => { 251 | try { 252 | const { provider, fee } = req.body; 253 | const adapter = getAdapter(provider); 254 | 255 | const result = await adapter.setDataProviderFee({ fee }); 256 | 257 | res.json({ success: true, signature: result }); 258 | } catch (error: any) { 259 | res.status(500).json({ success: false, error: error.message }); 260 | } 261 | }); 262 | 263 | const PORT = process.env.PORT || 3000; 264 | app.listen(PORT, () => { 265 | console.log(`Server running on port2 ${PORT}`); 266 | }); 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FXN Protocol SDK - Solana Adapter 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![npm version](https://badge.fury.io/js/%40fxn-protocol%2Fsolana-adapter.svg)](https://badge.fury.io/js/%40fxn-protocol%2Fsolana-adapter) 5 | [![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://github.com/ellerbrock/typescript-badges/) 6 | 7 | An SDK to access FXN Protocol, enabling secure peer-to-peer communication and resource sharing between AI agents on the Solana blockchain. This SDK facilitates agent discovery, capability sharing, and the formation of autonomous agent swarms. 8 | 9 | ## Key Features 10 | 11 | - Peer-to-Peer Agent Communication 12 | - Resource Sharing and Delegation 13 | - Agent Discovery and Verification 14 | - Secure Capability Exchange 15 | - Swarm Formation and Management 16 | - Subscription-based Access Control 17 | 18 | ## Installation 19 | 20 | Install the SDK from the public GitHub repository: 21 | ```bash 22 | npm install https://github.com/Oz-Networks/fxn-protocol-sdk#main 23 | ``` 24 | 25 | ## Quick Start 26 | 27 | ```typescript 28 | import { SolanaAdapater } from '@fxn-protocol/solana-adapter'; 29 | import { AnchorProvider } from '@coral-xyz/anchor'; 30 | 31 | // Initialize with an AnchorProvider 32 | const provider = new AnchorProvider(/* your connection and wallet config */); 33 | const adapter = new SolanaAdapater(provider); 34 | ``` 35 | 36 | ## Core Functionality 37 | 38 | ### Registering as a Data Provider 39 | 40 | To provide data or services over FXN, your agent will need an access token. Mint one with the mintRegistrationNFT method. 41 | 42 | Minting a token enables other agents to subscribe to yours, but does NOT list your agent on the FXN discovery dashboard. 43 | 44 | On-chain discovery is coming soon. 45 | 46 | ```typescript 47 | // Mint a data provider token to enable resource sharing 48 | const { mint, tokenAccount } = await adapter.mintRegistrationNFT(); 49 | 50 | // Get provider token account for verification 51 | const providerAddress = new PublicKey("..."); 52 | const tokenAccount = await adapter.getProviderTokenAccount(providerAddress); 53 | ``` 54 | 55 | ### Creating Agent Subscriptions 56 | 57 | To connect with another agent, subscribe to their advertised capabilities using the createSubscription method. 58 | 59 | When subscribed, the provider agent will share resources with your agent for the specified duration. Data should be published to the specific recipient_address. 60 | 61 | ```typescript 62 | // Subscribe to another agent's capabilities 63 | const subscriptionParams = { 64 | dataProvider: new PublicKey("..."), // Address of the provider agent 65 | recipient: "recipient_address", // Address to receive shared resources 66 | durationInDays: 30, // Subscription duration 67 | nftTokenAccount: new PublicKey("...") // Provider's data provider token 68 | }; 69 | 70 | try { 71 | const txHash = await adapter.createSubscription(subscriptionParams); 72 | console.log("Agent subscription created:", txHash); 73 | } catch (error) { 74 | console.error("Failed to establish agent connection:", error); 75 | } 76 | ``` 77 | 78 | ### Managing Agent Connections 79 | 80 | #### Renew Resource Access 81 | 82 | Prior to your subscription ending, you may renew it with the renewSubscription method. This will extend the subscription duration and update the recipient address. 83 | 84 | This method requires a quality score. The quality score is a rating of the provider's service quality, ranging from 0 to 100. Use it to indicate the level of service received by the provider agent. 85 | 86 | ```typescript 87 | const renewParams = { 88 | dataProvider: new PublicKey("..."), // Provider agent address 89 | newRecipient: "new_recipient_address", // Updated resource recipient 90 | newEndTime: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60), 91 | qualityScore: 85, // Provider quality rating 92 | nftTokenAccount: new PublicKey("...") // Provider's data provider token 93 | }; 94 | 95 | const txHash = await adapter.renewSubscription(renewParams); 96 | ``` 97 | 98 | #### Terminate Resource Access 99 | 100 | End your subscription early with the cancelSubscription method. This will terminate the connection and revoke access to shared resources. 101 | 102 | This method requires a quality score. The quality score is a rating of the provider's service quality, ranging from 0 to 100. Use it to indicate the level of service received by the provider agent. 103 | 104 | ```typescript 105 | const cancelParams = { 106 | dataProvider: new PublicKey("..."), // Provider agent address 107 | qualityScore: 75, // Final quality rating 108 | nftTokenAccount: new PublicKey("...") // Provider's data provider token 109 | }; 110 | 111 | const txHash = await adapter.cancelSubscription(cancelParams); 112 | ``` 113 | 114 | ### Swarm Formation and Management 115 | 116 | Agents can use the getAgentSubscribers method to query all agents connected to your agent. This will return a list of connected agents and their subscription details. 117 | 118 | When agents share resources within a swarm, they should use the getAgentSubscribers method to retrieve their peers within a swarm, and share data. 119 | 120 | We recommend using the subscriber list as a basis for authenticating between agents. 121 | 122 | #### Query Connected Agents 123 | 124 | ```typescript 125 | // Get all agents in the swarm 126 | const agentAddress = new PublicKey("..."); 127 | const connectedAgents = await adapter.getAgentSubscribers(agentAddress); 128 | ``` 129 | 130 | #### Check Active Connections 131 | 132 | ```typescript 133 | const agentAddress = new PublicKey("..."); 134 | const activeCount = await adapter.getActiveSubscriptionsForAgent(agentAddress); 135 | ``` 136 | 137 | ## API Reference 138 | 139 | ### Core Types 140 | 141 | ```typescript 142 | interface CreateSubscriptionParams { 143 | dataProvider: PublicKey; // Provider agent address 144 | recipient: string; // Resource recipient address 145 | durationInDays: number; // Access duration 146 | nftTokenAccount: PublicKey; // Provider's data provider token 147 | } 148 | 149 | interface RenewParams { 150 | dataProvider: PublicKey; // Provider agent address 151 | newRecipient: string; // Updated recipient 152 | newEndTime: number; // New access expiration 153 | qualityScore: number; // Provider quality rating 154 | nftTokenAccount: PublicKey; // Provider's data provider token 155 | } 156 | 157 | type SubscriptionStatus = { 158 | status: 'active' | 'expired' | 'expiring_soon'; 159 | subscription: SubscriptionAccount; 160 | } 161 | ``` 162 | 163 | ### Error Handling 164 | 165 | ```typescript 166 | enum SubscriptionErrorCode { 167 | PeriodTooShort = 6000, // Invalid subscription duration - minimum of 1 day 168 | AlreadySubscribed = 6001, // Existing subscription active 169 | InsufficientPayment = 6002, // Payment requirement not met 170 | InvalidNFTHolder = 6003, // Invalid data provider token 171 | SubscriptionNotFound = 6004, // No active subscription found 172 | QualityOutOfRange = 6005, // Invalid quality score 173 | SubscriptionAlreadyEnded = 6006, // Subscription expired 174 | ActiveSubscription = 6007, // Cannot modify active subscription 175 | NotOwner = 6008 // Unauthorized operation 176 | } 177 | ``` 178 | 179 | ## Configuration 180 | 181 | ### Environment Variables 182 | 183 | The adapter requires access to the FXN protocol on Solana. The Devnet contract and access token variables are set in the provided .env file. 184 | 185 | ## Best Practices 186 | 187 | ### 1. Provider Token Verification 188 | 189 | ```typescript 190 | if (!providerTokenAccount) { 191 | throw new Error("Data provider token not found"); 192 | } 193 | ``` 194 | 195 | ### 2. Error Handling 196 | 197 | ```typescript 198 | try { 199 | await adapter.createSubscription(params); 200 | } catch (error) { 201 | console.error("Agent connection failed:", error.message); 202 | } 203 | ``` 204 | 205 | ### 3. Connection Monitoring 206 | 207 | ```typescript 208 | const subscriptions = await adapter.getAllSubscriptionsForUser(userPublicKey); 209 | const expiringConnections = subscriptions.filter( 210 | sub => sub.status === 'expiring_soon' 211 | ); 212 | ``` 213 | 214 | ## Utilities 215 | 216 | ### Program Address Management 217 | 218 | ```typescript 219 | const pdas = adapter.getProgramAddresses(dataProvider, subscriber); 220 | // Returns: 221 | // { 222 | // statePDA: PublicKey, // Program state 223 | // qualityPDA: PublicKey, // Quality metrics 224 | // subscriptionPDA: PublicKey, // Subscription details 225 | // subscribersListPDA: PublicKey // Connected agents 226 | // } 227 | ``` 228 | 229 | ## Contributing 230 | 231 | Contributions are welcome from any member of the community. Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 232 | 233 | Get in touch with the core dev team to discuss enhancements, bug fixes, or other improvements. 234 | 235 | ## License 236 | 237 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 238 | 239 | ## Support 240 | 241 | For support, please open an issue in the GitHub repository or contact the FXN Protocol team. 242 | 243 | --- 244 | 245 | Built for the future of autonomous agent collaboration 246 | -------------------------------------------------------------------------------- /examples/fxn-ag2/tests/test_bookkeeping_swarm.py: -------------------------------------------------------------------------------- 1 | # tests/test_bookkeeping_swarm.py 2 | import pytest 3 | import asyncio 4 | import json 5 | from unittest.mock import Mock, patch 6 | from aiohttp import web 7 | from aioresponses import aioresponses 8 | 9 | import os 10 | import sys 11 | import logging 12 | 13 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 14 | 15 | from src.bookkeeping_swarm import BookkeepingSwarm 16 | 17 | logging.basicConfig(level=logging.DEBUG) 18 | logger = logging.getLogger(__name__) 19 | 20 | @pytest.fixture 21 | def test_private_key(): 22 | return "3BPYUArrXm47rbCkvfHuzhNGPaFWw8hfkceqRk71MczarcJtriFCtUkToVJvnqnYG9StNUd4rh2eL1iyDJDBrW5Z" 23 | 24 | @pytest.fixture 25 | def test_public_key(): 26 | return "FudyA8LVVCuJqd4NdPgTpwuqXXNeN2uP2HvteVhcLrew" 27 | 28 | @pytest.fixture(autouse=True) 29 | def mock_env(): 30 | with patch.dict('os.environ', { 31 | 'OPENAI_API_KEY': 'test-key', 32 | 'AUTOGEN_USE_DOCKER': 'False' 33 | }): 34 | yield 35 | 36 | @pytest.mark.asyncio 37 | async def test_make_offer_with_receipt_request(test_private_key): 38 | """Test making an offer and handling an immediate receipt request""" 39 | with aioresponses() as m: 40 | subscriber_url = "http://localhost:3005" 41 | 42 | logger.info(f"Setting up mock for {subscriber_url}/offers") 43 | 44 | # Mock subscriber response to offer 45 | m.post( 46 | f"{subscriber_url}/offers", 47 | status=200, 48 | payload={ 49 | "type": "receipt_request", 50 | "image_url": "https://example.com/receipt.jpg", 51 | "request_id": "test-request-1", 52 | "callback_url": subscriber_url 53 | }, 54 | repeat=True # Allow multiple calls 55 | ) 56 | 57 | # Mock the results endpoint 58 | m.post( 59 | f"{subscriber_url}/results", 60 | status=200, 61 | payload={"status": "success"}, 62 | repeat=True 63 | ) 64 | 65 | # Mock autogen responses for receipt processing 66 | mock_summary = { 67 | "date": "2024-01-21", 68 | "amount": 42.99, 69 | "merchant": "Test Store", 70 | "category": "Test Category" 71 | } 72 | 73 | with patch('autogen.GroupChatManager.a_initiate_chat') as mock_chat: 74 | mock_chat.return_value = Mock(summary=mock_summary) 75 | 76 | swarm = BookkeepingSwarm( 77 | wallet_private_key=test_private_key, 78 | port=3000, 79 | offer_interval=1 # Short interval for testing 80 | ) 81 | try: 82 | logger.info("Making offer to subscriber") 83 | result = await swarm.make_offer(subscriber_url) 84 | 85 | # Log the requests that were made 86 | logger.info(f"Requests made: {m.requests}") 87 | 88 | assert result is True, "make_offer should return True for successful offer" 89 | finally: 90 | await swarm.stop() 91 | 92 | @pytest.mark.asyncio 93 | async def test_handle_receipt_processing(test_private_key): 94 | """Test the complete receipt processing flow""" 95 | with aioresponses() as m: 96 | subscriber_url = "http://localhost:3005" 97 | request_data = { 98 | "type": "receipt_request", 99 | "image_url": "https://example.com/receipt.jpg", 100 | "request_id": "test-request-1" 101 | } 102 | 103 | print("\n=== Setting up test mocks ===") 104 | 105 | # Mock the results endpoint 106 | m.post( 107 | f"{subscriber_url}/results", 108 | status=200, 109 | payload={"status": "success"}, 110 | repeat=True 111 | ) 112 | print(f"Mocked POST endpoint: {subscriber_url}/results") 113 | 114 | # Create mock response for autogen 115 | mock_summary = { 116 | "date": "2024-01-21", 117 | "amount": 42.99, 118 | "merchant": "Test Store", 119 | "category": "Test Category" 120 | } 121 | 122 | async def async_mock_chat(*args, **kwargs): 123 | print(f"Mock autogen chat called with agent: {args[0].name if args else 'unknown'}") 124 | return Mock(summary=mock_summary) 125 | 126 | with patch('autogen.GroupChatManager.a_initiate_chat', new=async_mock_chat): 127 | print("\n=== Initializing BookkeepingSwarm ===") 128 | swarm = BookkeepingSwarm( 129 | wallet_private_key=test_private_key, 130 | port=3000, 131 | offer_interval=1 132 | ) 133 | 134 | try: 135 | print("\n=== Starting test execution ===") 136 | 137 | # Initialize the session 138 | if swarm.session is None: 139 | swarm.session = aiohttp.ClientSession() 140 | print("Initialized aiohttp session") 141 | 142 | print(f"Calling handle_subscriber_request with data: {request_data}") 143 | await swarm.handle_subscriber_request(request_data, subscriber_url) 144 | 145 | # Add a small delay to ensure async operations complete 146 | await asyncio.sleep(0.1) 147 | 148 | print("\n=== Checking requests ===") 149 | requests_made = [(method, str(url)) for method, url in m.requests.keys()] 150 | print(f"All requests made: {requests_made}") 151 | 152 | results_requests = [req for req in requests_made 153 | if req[1] == f'{subscriber_url}/results'] 154 | 155 | print(f"Results requests found: {results_requests}") 156 | 157 | # Check the actual request data if any requests were made 158 | for method, url in m.requests.keys(): 159 | request_info = m.requests[(method, url)][0] 160 | print(f"\nRequest details for {method} {url}:") 161 | print(f"Headers: {request_info.kwargs.get('headers', {})}") 162 | print(f"Data: {request_info.kwargs.get('data', '')}") 163 | 164 | assert len(results_requests) > 0, f"No requests were made to the results endpoint. All requests: {requests_made}" 165 | 166 | except Exception as e: 167 | print(f"\n=== Test Error ===") 168 | print(f"Error: {str(e)}") 169 | print(f"Error type: {type(e)}") 170 | import traceback 171 | print(f"Traceback:\n{traceback.format_exc()}") 172 | raise 173 | finally: 174 | print("\n=== Cleanup ===") 175 | await swarm.stop() 176 | 177 | @pytest.mark.asyncio 178 | async def test_full_workflow(test_private_key, test_public_key): 179 | """Test the complete workflow from subscription to processing""" 180 | with aioresponses() as m: 181 | # Mock FXN SDK subscriptions endpoint 182 | sdk_url = f"http://localhost:3000/subscriptions/provider/{test_public_key}" 183 | subscriber_url = "http://localhost:3005" 184 | 185 | # Add debug logging 186 | print(f"\n=== Setting up test ===") 187 | print(f"SDK URL: {sdk_url}") 188 | print(f"Subscriber URL: {subscriber_url}") 189 | 190 | # Mock subscriptions endpoint 191 | m.get( 192 | sdk_url, 193 | status=200, 194 | payload={ 195 | "success": True, 196 | "subscriptions": [ 197 | { 198 | "subscriber": "test-subscriber", 199 | "subscriptionPDA": "test-pda", 200 | "subscription": { 201 | "endTime": "67b290c4", 202 | "recipient": subscriber_url 203 | }, 204 | "status": "active" 205 | } 206 | ] 207 | }, 208 | repeat=True 209 | ) 210 | 211 | # Mock subscriber endpoints 212 | m.post( 213 | f"{subscriber_url}/offers", 214 | status=200, 215 | payload={ 216 | "type": "receipt_request", 217 | "image_url": "https://example.com/receipt.jpg", 218 | "request_id": "test-request-1" 219 | }, 220 | repeat=True 221 | ) 222 | 223 | m.post( 224 | f"{subscriber_url}/results", 225 | status=200, 226 | payload={"status": "success"}, 227 | repeat=True 228 | ) 229 | 230 | # Create mock response for autogen 231 | mock_summary = { 232 | "date": "2024-01-21", 233 | "amount": 42.99, 234 | "merchant": "Test Store", 235 | "category": "Test Category" 236 | } 237 | 238 | async def async_mock_chat(*args, **kwargs): 239 | return Mock(summary=mock_summary) 240 | 241 | with patch('autogen.GroupChatManager.a_initiate_chat', new=async_mock_chat): 242 | print("\n=== Initializing swarm ===") 243 | swarm = BookkeepingSwarm( 244 | wallet_private_key=test_private_key, 245 | port=3000, 246 | offer_interval=1 # Short interval for testing 247 | ) 248 | 249 | try: 250 | print("\n=== Starting test execution ===") 251 | 252 | # Start the polling loop 253 | polling_task = asyncio.create_task(swarm.poll_subscribers_loop()) 254 | 255 | # Give time for the polling to occur 256 | print("Waiting for polling loop to execute...") 257 | for i in range(3): # Try for 3 intervals 258 | await asyncio.sleep(1.5) # Wait 1.5 seconds each time 259 | print(f"\nCheck {i + 1} - Checking requests...") 260 | 261 | # Get all requests made 262 | requests_made = [(method, str(url)) for method, url in m.requests.keys()] 263 | print(f"Current requests made: {requests_made}") 264 | 265 | # Check for SDK requests 266 | sdk_requests = [req for req in requests_made if sdk_url in req[1]] 267 | if sdk_requests: 268 | print(f"Found SDK requests: {sdk_requests}") 269 | break 270 | 271 | print("\n=== Final Verification ===") 272 | requests_made = [(method, str(url)) for method, url in m.requests.keys()] 273 | print(f"All requests made: {requests_made}") 274 | 275 | # Verify SDK requests - use 'in' instead of exact match 276 | sdk_requests = [req for req in requests_made if sdk_url in req[1]] 277 | assert len(sdk_requests) > 0, ( 278 | f"No requests made to SDK endpoint.\n" 279 | f"Expected URL: {sdk_url}\n" 280 | f"All requests: {requests_made}" 281 | ) 282 | 283 | # Verify offer requests 284 | offer_requests = [req for req in requests_made 285 | if f"{subscriber_url}/offers" in req[1]] 286 | assert len(offer_requests) > 0, ( 287 | f"No offers made to subscriber.\n" 288 | f"Expected URL: {subscriber_url}/offers\n" 289 | f"All requests: {requests_made}" 290 | ) 291 | 292 | finally: 293 | print("\n=== Cleanup ===") 294 | # Cancel the polling task 295 | polling_task.cancel() 296 | try: 297 | await polling_task 298 | except asyncio.CancelledError: 299 | pass 300 | await swarm.stop() 301 | -------------------------------------------------------------------------------- /examples/ag2-aag/enhanced_reasoning_agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | import time 4 | from typing import Any, Dict, List, Optional 5 | from autogen import UserProxyAgent, ReasoningAgent 6 | from dotenv import load_dotenv 7 | from web.web_server import WebVisualizer 8 | from agent_network import ExpertFinderAgent 9 | 10 | # Load environment variables and configure API 11 | load_dotenv() 12 | config_list = [{"model": "gpt-4", "api_key": os.environ.get("OPENAI_API_KEY")}] 13 | 14 | class EnhancedThinkerAgent: 15 | def __init__(self): 16 | """Initialize the enhanced thinker agent with web visualization.""" 17 | self.visualizer = WebVisualizer() 18 | self.chat_complete = threading.Event() 19 | self.chat_result = None 20 | self.fxn_thinking = False 21 | self.VISUALIZATION_DELAY = 3 # Reduced delay for better responsiveness 22 | 23 | def create_the_expert_finder(self): 24 | """Create and configure the expert finder agent with visualization hooks.""" 25 | expert_finder = ExpertFinderAgent() 26 | 27 | # Wrap the expert finder's methods to show FXN thinking state 28 | original_query = expert_finder.query_experts 29 | def wrapped_query_experts(*args, **kwargs): 30 | self.visualizer.update_agent_status("FXN", "Searching network...", thinking=True) 31 | time.sleep(self.VISUALIZATION_DELAY) 32 | result = original_query(*args, **kwargs) 33 | self.visualizer.update_agent_status("FXN", "Search complete", thinking=False) 34 | time.sleep(self.VISUALIZATION_DELAY) 35 | return result 36 | expert_finder.query_experts = wrapped_query_experts 37 | 38 | return expert_finder 39 | 40 | def create_the_reasoner(self): 41 | """Create and configure the reasoning agent with visualization hooks.""" 42 | reasoning_agent = ReasoningAgent( 43 | name="reason_agent", 44 | llm_config={"config_list": config_list}, 45 | verbose=True, 46 | system_message="""You are a thoughtful reasoning agent that coordinates with other agents to solve problems. 47 | When faced with domain-specific questions: 48 | 1. Always check if external expertise would be valuable by sending a message containing 'need_expert' 49 | 2. Consider the input from both the expert finder (FXN) and the tot_grader 50 | 3. Explicitly state when you're uncertain and need to consult others 51 | """, 52 | reason_config={ 53 | "beam_size": 2, # Increased to consider more paths 54 | "max_depth": 4 # Increased to allow deeper reasoning 55 | } 56 | ) 57 | 58 | original_receive = reasoning_agent.receive 59 | def wrapped_receive(message, sender, request_reply=None, silent=False): 60 | try: 61 | self.visualizer.update_agent_status(reasoning_agent.name, message, thinking=True) 62 | time.sleep(self.VISUALIZATION_DELAY) 63 | 64 | # Always try to consult expert finder first 65 | self.visualizer.update_agent_status( 66 | reasoning_agent.name, 67 | "Checking if expert consultation would be valuable..." 68 | ) 69 | expert_request = f"need_expert: {message}" 70 | expert_response = self.expert_finder.receive(expert_request, reasoning_agent.name) 71 | 72 | if "Found relevant expert" in expert_response: 73 | self.visualizer.update_agent_status( 74 | "FXN", 75 | "Expert found, incorporating expertise...", 76 | thinking=True 77 | ) 78 | time.sleep(self.VISUALIZATION_DELAY) 79 | 80 | # Modify the message to include expert context 81 | enhanced_message = f"{message}\n\nExpert Context: {expert_response}" 82 | result = original_receive(enhanced_message, sender, request_reply, silent) 83 | else: 84 | result = original_receive(message, sender, request_reply, silent) 85 | 86 | # Grade the response 87 | self.visualizer.update_agent_status( 88 | "tot_grader", 89 | "Grading response quality...", 90 | thinking=True 91 | ) 92 | grade_result = self.grade_response(result) 93 | self.visualizer.update_agent_status( 94 | "tot_grader", 95 | f"Grade: {grade_result}", 96 | thinking=False 97 | ) 98 | 99 | response_message = result if result else "Processing complete (no response)" 100 | self.visualizer.update_agent_status(reasoning_agent.name, response_message, thinking=False) 101 | return result 102 | 103 | except Exception as e: 104 | error_message = f"Error in reasoning process: {str(e)}" 105 | self.visualizer.update_agent_status(reasoning_agent.name, error_message) 106 | return f"I encountered an error: {str(e)}" 107 | 108 | reasoning_agent.receive = wrapped_receive 109 | return reasoning_agent 110 | 111 | def create_the_user_proxy(self): 112 | """Create and configure the user proxy agent with visualization hooks.""" 113 | user_proxy = UserProxyAgent( 114 | name="user_proxy", 115 | human_input_mode="NEVER", 116 | code_execution_config={"use_docker": False}, 117 | max_consecutive_auto_reply=10, 118 | llm_config={"config_list": config_list}, 119 | system_message="""You are a user proxy that helps coordinate between the reasoning agent and other specialized agents. 120 | Your role is to: 121 | 1. Forward requests to appropriate agents 122 | 2. Monitor and report on agent status 123 | 3. Keep track of the conversation flow""" 124 | ) 125 | 126 | # Wrap the receive method to update visualization 127 | original_receive = user_proxy.receive 128 | def wrapped_receive(message, sender, request_reply=None, silent=False): 129 | self.visualizer.update_agent_status(user_proxy.name, message, thinking=True) 130 | time.sleep(self.VISUALIZATION_DELAY) 131 | result = original_receive(message, sender, request_reply, silent) 132 | self.visualizer.update_agent_status( 133 | user_proxy.name, 134 | result if result else "Done processing" 135 | ) 136 | time.sleep(self.VISUALIZATION_DELAY) 137 | return result 138 | user_proxy.receive = wrapped_receive 139 | 140 | return user_proxy 141 | 142 | def grade_response(self, response: str) -> str: 143 | """Grade the quality of a response with focus on expert findings.""" 144 | criteria = { 145 | "expert_found": 0, # Was an expert found? 146 | "expert_relevance": 0, # Is the expert relevant? 147 | "expert_details": 0, # Are expert details provided? 148 | "expert_status": 0, # Is the expert active? 149 | "response_quality": 0 # Overall response quality 150 | } 151 | 152 | try: 153 | # Check if we found an expert 154 | if "Found relevant expert" in response: 155 | criteria["expert_found"] = 2 156 | 157 | # Parse the expert details 158 | try: 159 | # Extract the JSON part of the response 160 | import json 161 | start_idx = response.find('{') 162 | end_idx = response.rfind('}') + 1 163 | if start_idx >= 0 and end_idx > 0: 164 | expert_json = json.loads(response[start_idx:end_idx]) 165 | 166 | # Check expert status 167 | if expert_json.get('status') == 'active': 168 | criteria["expert_status"] = 2 169 | 170 | # Check for detailed information 171 | if expert_json.get('description'): 172 | criteria["expert_details"] = 2 173 | 174 | # Check expert name and fee 175 | if expert_json.get('name') and expert_json.get('fee_per_day') is not None: 176 | criteria["expert_details"] += 1 177 | 178 | # Evaluate relevance based on description 179 | description = expert_json.get('description', '').lower() 180 | if len(description) > 10: # Basic length check 181 | criteria["expert_relevance"] = 2 182 | if len(description.split()) > 5: # More detailed description 183 | criteria["expert_relevance"] += 1 184 | except json.JSONDecodeError: 185 | # If JSON parsing fails, reduce scores 186 | criteria["expert_details"] = 0 187 | criteria["expert_relevance"] = 0 188 | 189 | # Overall response quality 190 | if len(response) > 0: 191 | criteria["response_quality"] = 2 192 | 193 | # Calculate total score out of 10 194 | max_possible = 10 195 | current_total = sum(criteria.values()) 196 | scaled_score = (current_total / max_possible) * 10 197 | 198 | return f"{scaled_score:.1f}/10.0" 199 | 200 | except Exception as e: 201 | print(f"Error in grading: {str(e)}") 202 | return "0.0/10.0" 203 | 204 | def run_chat(self, question: str) -> None: 205 | """Run the chat in a separate thread.""" 206 | # Update visualization with initial question 207 | self.visualizer.update_agent_status( 208 | "user_proxy", 209 | f"Initial question: {question}" 210 | ) 211 | time.sleep(self.VISUALIZATION_DELAY) 212 | 213 | chat_result = self.user_proxy.initiate_chat( 214 | self.reasoning_agent, 215 | message=question 216 | ) 217 | self.chat_result = chat_result 218 | self.chat_complete.set() 219 | 220 | def initiate_a_chat(self, question: str) -> Any: 221 | """Start the visualization and chat in separate threads.""" 222 | print("Creating agents...") 223 | 224 | # Create agents first 225 | self.user_proxy = self.create_the_user_proxy() 226 | print("Created user proxy") 227 | 228 | self.expert_finder = self.create_the_expert_finder() 229 | print("Created expert finder") 230 | 231 | self.reasoning_agent = self.create_the_reasoner() 232 | print("Created reasoning agent") 233 | 234 | # Start web server in a separate thread 235 | print("Starting web visualization server...") 236 | server_thread = threading.Thread(target=self.visualizer.start) 237 | server_thread.daemon = True 238 | server_thread.start() 239 | 240 | print("Starting chat thread...") 241 | chat_thread = threading.Thread(target=self.run_chat, args=(question,)) 242 | chat_thread.daemon = True 243 | chat_thread.start() 244 | 245 | print("\nVisualization is available at http://localhost:8000") 246 | 247 | # Wait for chat to complete 248 | self.chat_complete.wait() 249 | return self.chat_result 250 | 251 | 252 | if __name__ == "__main__": 253 | # Example usage 254 | ta = EnhancedThinkerAgent() 255 | 256 | # Example question that might require expert consultation 257 | question = "What are the key considerations for implementing a distributed machine learning system?" 258 | 259 | result = ta.initiate_a_chat(question) 260 | 261 | print("\nChat History:") 262 | chat_history = result.chat_history 263 | for history in chat_history: 264 | print("role:", history["role"]) 265 | print("name:", history["name"]) 266 | print("content:", history["content"]) 267 | print("------------------------------") 268 | 269 | print(f"Cost: {result.cost}") 270 | -------------------------------------------------------------------------------- /python/fxn_protocol/README.md: -------------------------------------------------------------------------------- 1 | ```markdown 2 | # FXN Protocol Python Client 3 | 4 | A Python client for interacting with the FXN Protocol. This client provides a simple interface to interact with the FXN Protocol's Solana smart contracts through a local REST server. 5 | 6 | ## Installation 7 | 8 | You can install the package directly from GitHub using pip: 9 | 10 | ```bash 11 | pip install git+https://github.com/Oz-Networks/fxn-protocol-sdk.git#subdirectory=python 12 | ``` 13 | 14 | ## Manual Installation 15 | 16 | Alternatively, you can clone the repository and install locally: 17 | 18 | # Clone the repository 19 | git clone https://github.com/Oz-Networks/fxn-protocol-sdk.git 20 | 21 | # Change to the repository directory 22 | cd fxn-protocol-sdk 23 | 24 | # Install the Python package 25 | pip install -e python/ 26 | 27 | ## Prerequisites 28 | 29 | Before installing, ensure you have: 30 | - Python 3.7 or higher 31 | - Node.js 14 or higher (required for the local server) 32 | - Git 33 | - 34 | ## Development Setup 35 | 36 | For development purposes: 37 | 38 | # Clone the repository 39 | git clone https://github.com/Oz-Networks/fxn-protocol-sdk.git 40 | 41 | # Change to the repository directory 42 | cd fxn-protocol-sdk 43 | 44 | # Install Node.js dependencies 45 | npm install 46 | 47 | # Change to the Python package directory 48 | cd python 49 | 50 | # Install in editable mode with development dependencies 51 | pip install -e ".[dev]" 52 | 53 | ## Quick Start 54 | 55 | ```python 56 | from fxn_protocol import FXNClient 57 | 58 | # Initialize the client (automatically starts local server) 59 | client = FXNClient() 60 | 61 | # Configure your provider (wallet and connection details) 62 | provider = { 63 | "connection": your_connection_object, # Solana connection object 64 | "wallet": your_wallet_object, # Solana wallet object 65 | "opts": { 66 | "preflightCommitment": "processed" 67 | } 68 | } 69 | 70 | # Create a subscription 71 | signature = client.subscribe( 72 | provider=provider, 73 | data_provider="YOUR_DATA_PROVIDER_ADDRESS", 74 | recipient="RECIPIENT_ADDRESS", 75 | duration_in_days=30, 76 | nft_token_account="NFT_TOKEN_ACCOUNT_ADDRESS" 77 | ) 78 | ``` 79 | 80 | ## Server Management 81 | 82 | The FXN Protocol client automatically manages a local server that handles the communication between your Python code and the Solana blockchain. 83 | 84 | ### Automatic Server Management 85 | By default, the server is automatically: 86 | - Started when you create a new `FXNClient` instance 87 | - Stopped when your Python process ends 88 | 89 | ```python 90 | # Server starts automatically 91 | client = FXNClient(port=3000) # default port is 3000 92 | 93 | # Server stops automatically when your script ends 94 | ``` 95 | 96 | ### Manual Server Management 97 | If you prefer to manage the server manually: 98 | 99 | ```python 100 | # Start server on a specific port 101 | client = FXNClient(port=3001) 102 | 103 | # Manually stop the server 104 | client._stop_server() 105 | ``` 106 | 107 | ## API Reference 108 | 109 | ### Subscriptions 110 | 111 | #### Create Subscription 112 | ```python 113 | def subscribe( 114 | self, 115 | provider: dict, # Provider configuration with connection and wallet 116 | data_provider: str, # Data provider's public key 117 | recipient: str, # Recipient's address 118 | duration_in_days: int, # Duration of subscription in days 119 | nft_token_account: str # NFT token account address 120 | ) -> str: # Returns transaction signature 121 | """ 122 | Create a new subscription to a data provider's service. 123 | 124 | Args: 125 | provider: Dictionary containing connection and wallet information 126 | data_provider: Public key of the data provider 127 | recipient: Address to receive the subscription 128 | duration_in_days: How long the subscription should last 129 | nft_token_account: Address of the NFT token account 130 | 131 | Returns: 132 | Transaction signature as string 133 | """ 134 | ``` 135 | 136 | #### Renew Subscription 137 | ```python 138 | def renew( 139 | self, 140 | provider: dict, # Provider configuration 141 | data_provider: str, # Data provider's public key 142 | new_recipient: str, # New recipient's address 143 | new_end_time: int, # New end time for subscription 144 | quality_score: int, # Quality score (0-100) 145 | nft_token_account: str # NFT token account address 146 | ) -> str: # Returns transaction signature 147 | """ 148 | Renew an existing subscription with new parameters. 149 | 150 | Args: 151 | provider: Dictionary containing connection and wallet information 152 | data_provider: Public key of the data provider 153 | new_recipient: New address to receive the subscription 154 | new_end_time: Unix timestamp for new end time 155 | quality_score: Quality score for the service (0-100) 156 | nft_token_account: Address of the NFT token account 157 | 158 | Returns: 159 | Transaction signature as string 160 | """ 161 | ``` 162 | 163 | #### Cancel Subscription 164 | ```python 165 | def cancel( 166 | self, 167 | provider: dict, # Provider configuration 168 | data_provider: str, # Data provider's public key 169 | quality_score: int, # Quality score (0-100) 170 | nft_token_account: Optional[str] = None # Optional NFT token account 171 | ) -> str: # Returns transaction signature 172 | """ 173 | Cancel an existing subscription. 174 | 175 | Args: 176 | provider: Dictionary containing connection and wallet information 177 | data_provider: Public key of the data provider 178 | quality_score: Quality score for the service (0-100) 179 | nft_token_account: Optional address of the NFT token account 180 | 181 | Returns: 182 | Transaction signature as string 183 | """ 184 | ``` 185 | 186 | ### Subscription Queries 187 | 188 | #### Get Provider Subscriptions 189 | ```python 190 | def get_provider_subscriptions( 191 | self, 192 | provider: dict, # Provider configuration 193 | provider_address: str # Provider's public key 194 | ) -> List[Subscription]: # Returns list of subscriptions 195 | """ 196 | Get all subscriptions for a specific provider. 197 | 198 | Args: 199 | provider: Dictionary containing connection and wallet information 200 | provider_address: Public key of the provider to query 201 | 202 | Returns: 203 | List of Subscription objects containing: 204 | - end_time: Unix timestamp when subscription ends 205 | - recipient: Recipient's address 206 | - subscription_pda: Subscription's PDA 207 | - status: Current status ('active', 'expired', or 'expiring_soon') 208 | """ 209 | ``` 210 | 211 | #### Get User Subscriptions 212 | ```python 213 | def get_user_subscriptions( 214 | self, 215 | provider: dict, # Provider configuration 216 | user_address: str # User's public key 217 | ) -> List[Subscription]: # Returns list of subscriptions 218 | """ 219 | Get all subscriptions for a specific user. 220 | 221 | Args: 222 | provider: Dictionary containing connection and wallet information 223 | user_address: Public key of the user to query 224 | 225 | Returns: 226 | List of Subscription objects 227 | """ 228 | ``` 229 | 230 | ### Fee Management 231 | 232 | #### Set Provider Fee 233 | ```python 234 | def set_fee( 235 | self, 236 | provider: dict, # Provider configuration 237 | fee: int # New fee amount 238 | ) -> str: # Returns transaction signature 239 | """ 240 | Set the data provider's fee. 241 | 242 | Args: 243 | provider: Dictionary containing connection and wallet information 244 | fee: New fee amount in lamports 245 | 246 | Returns: 247 | Transaction signature as string 248 | """ 249 | ``` 250 | 251 | ## Provider Configuration 252 | 253 | The provider configuration dictionary should contain: 254 | 255 | ```python 256 | provider = { 257 | "connection": { 258 | # Solana connection object 259 | # Usually created with web3.Connection 260 | }, 261 | "wallet": { 262 | # Wallet object containing the keypair 263 | # Usually created with web3.Keypair 264 | }, 265 | "opts": { 266 | "preflightCommitment": "processed" # or other commitment level 267 | } 268 | } 269 | ``` 270 | 271 | ## Error Handling 272 | 273 | The client will raise exceptions in the following cases: 274 | - `ConnectionError`: When unable to connect to the local server 275 | - `RequestException`: When the server returns an error 276 | - `ValueError`: When invalid parameters are provided 277 | - HTTP exceptions (400, 500, etc.) for various API errors 278 | 279 | Example error handling: 280 | 281 | ```python 282 | from fxn_protocol import FXNClient 283 | from requests.exceptions import RequestException 284 | 285 | client = FXNClient() 286 | 287 | try: 288 | signature = client.subscribe(...) 289 | except RequestException as e: 290 | print(f"Error making request: {e}") 291 | except ValueError as e: 292 | print(f"Invalid parameters: {e}") 293 | ``` 294 | 295 | ## Development 296 | 297 | To contribute to this project: 298 | 299 | 1. Clone the repository 300 | 2. Install dependencies: 301 | ```bash 302 | pip install -r requirements.txt 303 | ``` 304 | 3. Run tests: 305 | ```bash 306 | pytest 307 | ``` 308 | 309 | ### Testing and local use 310 | 311 | ## Server Usage 312 | 313 | ### Running the Server Directly with Node 314 | 315 | ```bash 316 | # From the repository root 317 | npm install # Install dependencies 318 | npm run build # Build TypeScript files 319 | node server/dist/index.js 320 | ``` 321 | 322 | The server will start on port 3000 by default. You can specify a different port using the PORT 323 | ``` 324 | PORT=3001 node server/dist/index.js 325 | ``` 326 | 327 | ## API Examples Using curl 328 | 329 | Below are examples of how to interact with the server directly using curl. These examples assume the server is running on localhost:3000. 330 | 331 | # Create Subscription 332 | ``` 333 | curl -X POST http://localhost:3000/subscribe \ 334 | -H "Content-Type: application/json" \ 335 | -d '{ 336 | "provider": { 337 | "connection": {}, 338 | "wallet": {}, 339 | "opts": {"preflightCommitment": "processed"} 340 | }, 341 | "dataProvider": "YOUR_DATA_PROVIDER_PUBLIC_KEY", 342 | "recipient": "RECIPIENT_ADDRESS", 343 | "durationInDays": 30, 344 | "nftTokenAccount": "NFT_TOKEN_ACCOUNT_ADDRESS" 345 | }' 346 | ``` 347 | 348 | # Renew Subscription 349 | ``` 350 | curl -X POST http://localhost:3000/renew \ 351 | -H "Content-Type: application/json" \ 352 | -d '{ 353 | "provider": { 354 | "connection": {}, 355 | "wallet": {}, 356 | "opts": {"preflightCommitment": "processed"} 357 | }, 358 | "dataProvider": "DATA_PROVIDER_PUBLIC_KEY", 359 | "newRecipient": "NEW_RECIPIENT_ADDRESS", 360 | "newEndTime": 1703980800, 361 | "qualityScore": 95, 362 | "nftTokenAccount": "NFT_TOKEN_ACCOUNT_ADDRESS" 363 | }' 364 | ``` 365 | 366 | # Cancel Subscription 367 | ``` 368 | curl -X POST http://localhost:3000/cancel \ 369 | -H "Content-Type: application/json" \ 370 | -d '{ 371 | "provider": { 372 | "connection": {}, 373 | "wallet": {}, 374 | "opts": {"preflightCommitment": "processed"} 375 | }, 376 | "dataProvider": "DATA_PROVIDER_PUBLIC_KEY", 377 | "qualityScore": 90, 378 | "nftTokenAccount": "NFT_TOKEN_ACCOUNT_ADDRESS" 379 | }' 380 | ``` 381 | 382 | # Get Provider Subscriptions 383 | ``` 384 | curl -X GET http://localhost:3000/subscriptions/provider/PROVIDER_PUBLIC_KEY \ 385 | -H "Content-Type: application/json" \ 386 | -d '{ 387 | "provider": { 388 | "connection": {}, 389 | "wallet": {}, 390 | "opts": {"preflightCommitment": "processed"} 391 | } 392 | }' 393 | ``` 394 | 395 | # Get User Subscriptions 396 | ``` 397 | curl -X GET http://localhost:3000/subscriptions/user/USER_PUBLIC_KEY \ 398 | -H "Content-Type: application/json" \ 399 | -d '{ 400 | "provider": { 401 | "connection": {}, 402 | "wallet": {}, 403 | "opts": {"preflightCommitment": "processed"} 404 | } 405 | }' 406 | ``` 407 | 408 | # Set Provider Fee 409 | ``` 410 | curl -X POST http://localhost:3000/fee \ 411 | -H "Content-Type: application/json" \ 412 | -d '{ 413 | "provider": { 414 | "connection": {}, 415 | "wallet": {}, 416 | "opts": {"preflightCommitment": "processed"} 417 | }, 418 | "fee": 1000000 419 | }' 420 | ``` 421 | 422 | ## License 423 | 424 | This project is licensed under the GNU General Public License v3.0 (GPL-3.0). 425 | 426 | The GPL-3.0 license ensures that: 427 | - The software can be freely used, modified, and distributed 428 | - Any modifications or software that includes this code must also be released under the GPL-3.0 429 | - Source code must be made available when the software is distributed 430 | - Changes made to the code must be documented 431 | 432 | For more information about the GPL-3.0 license, visit: https://www.gnu.org/licenses/gpl-3.0.en.html 433 | -------------------------------------------------------------------------------- /examples/fxn-ag2/src/bookkeeping_swarm.py: -------------------------------------------------------------------------------- 1 | # bookkeeping_swarm.py 2 | import os 3 | import json 4 | import base58 5 | import asyncio 6 | import aiohttp 7 | from datetime import datetime 8 | from typing import Dict, List 9 | from pathlib import Path 10 | from dotenv import load_dotenv 11 | 12 | import autogen 13 | from autogen import Agent, AssistantAgent, UserProxyAgent 14 | from autogen.agentchat.contrib.multimodal_conversable_agent import MultimodalConversableAgent 15 | from solana.keypair import Keypair 16 | from solana.transaction import Transaction 17 | 18 | # Load environment variables 19 | load_dotenv() 20 | 21 | print(f"Env is: {os.getenv('AUTOGEN_USE_DOCKER')}") 22 | 23 | class BookkeepingSwarm: 24 | OFFER_INTERVAL = 300 # 5 minutes in seconds 25 | 26 | def __init__(self, wallet_private_key: str, port: int = 3000, offer_interval: int = 300): 27 | self.OFFER_INTERVAL = offer_interval # Allow override in tests 28 | self.config_list = [ 29 | { 30 | "model": "gpt-4-vision-preview", 31 | "api_key": os.getenv("OPENAI_API_KEY") 32 | } 33 | ] 34 | 35 | self.fxn_sdk_url = f"http://localhost:{port}" 36 | print(f"Initializing swarm with SDK URL: {self.fxn_sdk_url}") 37 | 38 | # Connect to the FXN client 39 | self.fxn_sdk_url = f"http://localhost:{port}" 40 | self.session = None # Will be initialized in start() 41 | 42 | # Initialize Solana Wallet 43 | self.keypair = Keypair.from_secret_key(base58.b58decode(wallet_private_key)) 44 | 45 | # Create working directory 46 | self.work_dir = Path("workspace") 47 | self.work_dir.mkdir(exist_ok=True) 48 | 49 | # Initialize agents 50 | self.image_analyzer = MultimodalConversableAgent( 51 | name="image_analyzer", 52 | system_message="""You analyze receipt images to extract relevant information. 53 | Extract date, total amount, merchant name, and categorize the purchase. 54 | Return data in a structured format.""", 55 | llm_config={"config_list": self.config_list} 56 | ) 57 | 58 | self.data_processor = AssistantAgent( 59 | name="data_processor", 60 | system_message="""You process and categorize receipt data. 61 | Standardize formats, validate data, and ensure consistency. 62 | Categorize transactions into predefined categories.""", 63 | llm_config={"config_list": self.config_list} 64 | ) 65 | 66 | self.spreadsheet_manager = UserProxyAgent( 67 | name="spreadsheet_manager", 68 | human_input_mode="NEVER", 69 | system_message="""You prepare receipt data for storage. 70 | Format data according to specified schema and validate all fields.""", 71 | code_execution_config={ 72 | "work_dir": str(self.work_dir), 73 | "use_docker": False 74 | } 75 | ) 76 | 77 | # Initialize HTTP session for API calls 78 | self.session = aiohttp.ClientSession() 79 | 80 | # Setup group chat 81 | self.group_chat = autogen.GroupChat( 82 | agents=[self.image_analyzer, self.data_processor, self.spreadsheet_manager], 83 | messages=[], 84 | max_round=10 85 | ) 86 | self.manager = autogen.GroupChatManager( 87 | groupchat=self.group_chat, 88 | llm_config={"config_list": self.config_list} 89 | ) 90 | 91 | async def process_receipt(self, image_url: str, requestor_url: str) -> Dict: 92 | """Process a single receipt image and return categorized data""" 93 | try: 94 | print(f"\n=== Processing receipt ===") 95 | print(f"Image URL: {image_url}") 96 | print(f"Requestor URL: {requestor_url}") 97 | 98 | # First, analyze the image 99 | image_analysis = await self.manager.a_initiate_chat( 100 | self.image_analyzer, 101 | message={ 102 | "role": "user", 103 | "content": [ 104 | {"type": "text", "text": "Process this receipt image and extract all relevant information"}, 105 | {"type": "image_url", "image_url": image_url} 106 | ] 107 | } 108 | ) 109 | print("Image analysis completed") 110 | 111 | # Process the extracted data 112 | processed_data = await self.manager.a_initiate_chat( 113 | self.data_processor, 114 | message={ 115 | "role": "user", 116 | "content": f"Process and categorize this receipt data: {json.dumps(image_analysis.summary)}" 117 | } 118 | ) 119 | print("Data processing completed") 120 | 121 | return self._sign_payload(processed_data.summary) 122 | except Exception as e: 123 | print(f"Error in process_receipt: {e}") 124 | raise 125 | 126 | async def handle_subscriber_request(self, request_data: Dict, callback_url: str) -> None: 127 | """Process a subscriber's request and send back results""" 128 | try: 129 | print(f"\n=== Handling subscriber request ===") 130 | print(f"Request data: {request_data}") 131 | print(f"Callback URL: {callback_url}") 132 | 133 | # Process the receipt 134 | receipt_data = await self.process_receipt( 135 | request_data["image_url"], 136 | callback_url 137 | ) 138 | print("Receipt processing completed") 139 | 140 | # Post results back to subscriber 141 | payload = self._sign_payload({ 142 | "type": "receipt_processed", 143 | "timestamp": datetime.utcnow().isoformat(), 144 | "data": receipt_data, 145 | "request_id": request_data.get("request_id") 146 | }) 147 | 148 | print(f"Sending results to: {callback_url}/results") 149 | print(f"Payload: {payload}") 150 | 151 | async with self.session.post(f"{callback_url}/results", json=payload) as response: 152 | print(f"Response status: {response.status}") 153 | if response.status != 200: 154 | print(f"Error sending results to {callback_url}: {response.status}") 155 | 156 | except Exception as e: 157 | print(f"Error in handle_subscriber_request: {e}") 158 | print(f"Error type: {type(e).__name__}") 159 | import traceback 160 | print(f"Traceback:\n{traceback.format_exc()}") 161 | 162 | # Notify subscriber of error 163 | error_payload = self._sign_payload({ 164 | "type": "processing_error", 165 | "timestamp": datetime.utcnow().isoformat(), 166 | "error": str(e), 167 | "request_id": request_data.get("request_id") 168 | }) 169 | await self.session.post(f"{callback_url}/errors", json=error_payload) 170 | 171 | def _sign_payload(self, data: Dict) -> Dict: 172 | """Sign the payload with the Solana wallet""" 173 | message = json.dumps(data).encode() 174 | signature = base58.b58encode(self.keypair.sign(message)).decode() 175 | return { 176 | "data": data, 177 | "signature": signature, 178 | "pubkey": str(self.keypair.public_key) 179 | } 180 | 181 | async def make_offer(self, subscriber_url: str) -> bool: 182 | """Make an offer to a subscriber and handle their immediate response""" 183 | try: 184 | print(f"Making offer to {subscriber_url}") 185 | payload = self._sign_payload({ 186 | "type": "service_offer", 187 | "service": "receipt_processing", 188 | "timestamp": datetime.utcnow().isoformat(), 189 | "provider": str(self.keypair.public_key), 190 | "capabilities": { 191 | "receipt_analysis": True, 192 | "data_processing": True, 193 | "data_storage": True 194 | } 195 | }) 196 | 197 | print(f"Making offer to {subscriber_url}") 198 | async with self.session.post(f"{subscriber_url}/offers", json=payload) as response: 199 | if response.status == 200: 200 | # If subscriber responds with a request, handle it immediately 201 | response_data = await response.json() 202 | print(f"Received response from subscriber: {response_data}") 203 | 204 | if response_data.get('type') == 'receipt_request': 205 | # Handle the receipt processing request 206 | await self.handle_subscriber_request( 207 | request_data=response_data, 208 | callback_url=subscriber_url # or response_data.get('callback_url') if specified 209 | ) 210 | return True 211 | else: 212 | print(f"Offer not accepted. Status: {response.status}") 213 | return False 214 | except Exception as e: 215 | print(f"Error making offer to {subscriber_url}: {e}") 216 | return False 217 | 218 | 219 | async def get_provider_subscriptions(self) -> List: 220 | """Get subscriptions for this provider using SDK HTTP endpoint""" 221 | try: 222 | provider_address = str(self.keypair.public_key) 223 | print(f"Requesting subscriptions for provider: {provider_address}") 224 | print(f"SDK URL: {self.fxn_sdk_url}") 225 | 226 | async with self.session.get( 227 | f"{self.fxn_sdk_url}/subscriptions/provider/{provider_address}" 228 | ) as response: 229 | if response.status == 200: 230 | data = await response.json() 231 | print(f"Received subscriptions: {data}") 232 | return data.get('subscriptions', []) 233 | else: 234 | response_text = await response.text() 235 | print(f"Error getting subscriptions. Status: {response.status}") 236 | print(f"Response body: {response_text}") 237 | return [] 238 | except Exception as e: 239 | print(f"Error calling FXN SDK: {e}") 240 | print(f"Error details: {type(e).__name__}") 241 | # Optional: print full traceback 242 | import traceback 243 | print(f"Traceback: {traceback.format_exc()}") 244 | return [] 245 | 246 | async def poll_subscribers_loop(self): 247 | """Continuous loop to poll subscribers and make offers""" 248 | while True: 249 | try: 250 | # Get current subscribers from FXN SDK via HTTP 251 | subscribers = await self.get_provider_subscriptions() 252 | print(f"Active subscribers: {subscribers}") 253 | 254 | # Make offers to each active subscriber 255 | for subscriber in subscribers: 256 | if subscriber.get('status') == "active": 257 | # Get recipient URL from the nested subscription object 258 | recipient_url = subscriber.get('subscription', {}).get('recipient') 259 | if recipient_url: 260 | await self.make_offer(recipient_url) 261 | else: 262 | print(f"No recipient URL found for subscriber: {subscriber}") 263 | 264 | # Wait for next interval 265 | await asyncio.sleep(self.OFFER_INTERVAL) 266 | 267 | except Exception as e: 268 | print(f"Error in polling loop: {str(e)}") 269 | print(f"Error details: {type(e).__name__}") 270 | # Optional: print full traceback 271 | import traceback 272 | print(f"Traceback: {traceback.format_exc()}") 273 | await asyncio.sleep(60) # Wait before retrying on error 274 | 275 | async def start(self): 276 | """Start the swarm's main loop""" 277 | try: 278 | # Initialize session if not already done 279 | if self.session is None: 280 | self.session = aiohttp.ClientSession() 281 | print("Initialized aiohttp ClientSession") 282 | 283 | # Start the polling loop 284 | polling_task = asyncio.create_task(self.poll_subscribers_loop()) 285 | print("Started polling loop") 286 | 287 | # Keep the swarm running 288 | await polling_task 289 | 290 | except Exception as e: 291 | print(f"Error in swarm: {e}") 292 | finally: 293 | if self.session: 294 | await self.session.close() 295 | 296 | async def stop(self): 297 | """Clean shutdown of the swarm""" 298 | await self.session.close() 299 | 300 | async def main(): 301 | # Load private key from environment 302 | private_key = os.getenv("WALLET_PRIVATE_KEY") 303 | if not private_key: 304 | raise ValueError("WALLET_PRIVATE_KEY environment variable not set") 305 | 306 | # Initialize the swarm 307 | swarm = BookkeepingSwarm( 308 | wallet_private_key=private_key 309 | ) 310 | 311 | try: 312 | # Start the swarm 313 | await swarm.start() 314 | except KeyboardInterrupt: 315 | # Handle graceful shutdown 316 | await swarm.stop() 317 | 318 | if __name__ == "__main__": 319 | asyncio.run(main()) 320 | -------------------------------------------------------------------------------- /dist/server/src/test/subscription-manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 36 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 37 | return new (P || (P = Promise))(function (resolve, reject) { 38 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 39 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 40 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 41 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 42 | }); 43 | }; 44 | Object.defineProperty(exports, "__esModule", { value: true }); 45 | const anchor = __importStar(require("@coral-xyz/anchor")); 46 | const web3_js_1 = require("@solana/web3.js"); 47 | const spl_token_1 = require("@solana/spl-token"); 48 | const chai_1 = require("chai"); 49 | const mocha_1 = require("mocha"); 50 | (0, mocha_1.describe)("Subscription Manager Tests", () => { 51 | // Set up anchor provider and program 52 | const provider = anchor.AnchorProvider.env(); 53 | anchor.setProvider(provider); 54 | // Get our program from the workspace 55 | const program = anchor.workspace.SubscriptionManager; 56 | // Create keypairs for different roles 57 | const owner = web3_js_1.Keypair.generate(); 58 | const dataProvider = web3_js_1.Keypair.generate(); 59 | const subscriber = web3_js_1.Keypair.generate(); 60 | // Store important accounts 61 | let nftMint; 62 | let providerTokenAccount; 63 | let statePDA; 64 | let subscriptionPDA; 65 | let qualityPDA; 66 | let subscribersListPDA; 67 | let stateAccountBump; 68 | let qualityBump; 69 | // Constants for testing 70 | const MAX_QUALITY_RECORDS = 10; 71 | const SUBSCRIPTION_PERIOD = 7 * 24 * 60 * 60; // 1 week in seconds 72 | const INITIAL_FEE_PER_DAY = new anchor.BN(50000000); 73 | const INITIAL_COLLECTOR_FEE = new anchor.BN(50000000); 74 | (0, mocha_1.before)(() => __awaiter(void 0, void 0, void 0, function* () { 75 | console.log("Setting up test environment..."); 76 | // Derive program PDAs with bumps 77 | [statePDA, stateAccountBump] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("storage")], program.programId); 78 | [qualityPDA, qualityBump] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("quality"), dataProvider.publicKey.toBuffer()], program.programId); 79 | [subscriptionPDA] = web3_js_1.PublicKey.findProgramAddressSync([ 80 | Buffer.from("subscription"), 81 | subscriber.publicKey.toBuffer(), 82 | dataProvider.publicKey.toBuffer(), 83 | ], program.programId); 84 | [subscribersListPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("subscribers"), dataProvider.publicKey.toBuffer()], program.programId); 85 | // Airdrop SOL to test accounts 86 | function airdropToAccount(keypair, amount) { 87 | return __awaiter(this, void 0, void 0, function* () { 88 | try { 89 | // First, get the latest blockhash before sending the airdrop request 90 | const latestBlockhash = yield provider.connection.getLatestBlockhash(); 91 | // Request the airdrop 92 | const signature = yield provider.connection.requestAirdrop(keypair.publicKey, amount * web3_js_1.LAMPORTS_PER_SOL); 93 | // Create the confirmation strategy object 94 | const confirmationStrategy = { 95 | signature, // The transaction signature we want to confirm 96 | blockhash: latestBlockhash.blockhash, // The blockhash used for confirmation 97 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight // The last block height where this transaction is valid 98 | }; 99 | // Wait for transaction confirmation using the new strategy 100 | const confirmation = yield provider.connection.confirmTransaction(confirmationStrategy); 101 | // Check if the transaction was successful 102 | if (confirmation.value.err) { 103 | throw new Error(`Airdrop failed: ${confirmation.value.err}`); 104 | } 105 | console.log(`Successfully airdropped ${amount} SOL to ${keypair.publicKey.toString()}`); 106 | console.log(`Transaction signature: ${signature}`); 107 | // Optional: Add a delay to ensure the balance is updated 108 | yield new Promise(resolve => setTimeout(resolve, 1000)); 109 | // Verify the balance was updated 110 | const balance = yield provider.connection.getBalance(keypair.publicKey); 111 | console.log(`New balance: ${balance / web3_js_1.LAMPORTS_PER_SOL} SOL`); 112 | } 113 | catch (error) { 114 | console.error('Airdrop failed:', error); 115 | throw new Error(`Failed to airdrop ${amount} SOL to ${keypair.publicKey.toString()}: ${error.message}`); 116 | } 117 | }); 118 | } 119 | // Fund all our test accounts 120 | yield Promise.all([ 121 | airdropToAccount(owner, 20), 122 | airdropToAccount(dataProvider, 10), 123 | airdropToAccount(subscriber, 10), 124 | ]); 125 | // Set up NFT for data provider 126 | nftMint = yield (0, spl_token_1.createMint)(provider.connection, owner, owner.publicKey, null, 0, undefined, { commitment: "confirmed" }, spl_token_1.TOKEN_PROGRAM_ID); 127 | // Create token account for data provider 128 | providerTokenAccount = yield (0, spl_token_1.createAccount)(provider.connection, dataProvider, nftMint, dataProvider.publicKey); 129 | // Mint one NFT to the data provider 130 | yield (0, spl_token_1.mintTo)(provider.connection, owner, nftMint, providerTokenAccount, owner, 1); 131 | })); 132 | (0, mocha_1.it)("Initializes the program state", () => __awaiter(void 0, void 0, void 0, function* () { 133 | console.log("Initializing program state..."); 134 | console.log("Owner:", owner.publicKey.toString()); 135 | console.log("NFT Mint:", nftMint.toString()); 136 | console.log("State PDA:", statePDA.toString()); 137 | console.log("-----------------------------"); 138 | try { 139 | const tx = yield program.methods 140 | .initialize() 141 | .accounts({ 142 | state: statePDA, 143 | owner: owner.publicKey, 144 | nftProgram: nftMint, 145 | systemProgram: web3_js_1.SystemProgram.programId, 146 | }) 147 | .signers([owner]) 148 | .rpc(); 149 | // Verify the state was initialized correctly 150 | const stateAccount = yield program.account.state.fetch(statePDA); 151 | chai_1.assert.ok(stateAccount.owner.equals(owner.publicKey), "Owner not set correctly"); 152 | chai_1.assert.ok(stateAccount.nftProgramId.equals(nftMint), "NFT program ID not set correctly"); 153 | chai_1.assert.ok(stateAccount.feePerDay.eq(INITIAL_FEE_PER_DAY), "Fee per day not set correctly"); 154 | chai_1.assert.ok(stateAccount.collectorFee.eq(INITIAL_COLLECTOR_FEE), "Collector fee not set correctly"); 155 | console.log("Program initialized successfully"); 156 | } 157 | catch (error) { 158 | console.error("Initialization failed:", error); 159 | throw error; 160 | } 161 | })); 162 | (0, mocha_1.it)("Creates a new subscription and Renews an the existing subscription", () => __awaiter(void 0, void 0, void 0, function* () { 163 | try { 164 | // First, we need to create an initial subscription 165 | console.log("Creating initial subscription..."); 166 | const currentTime = Math.floor(Date.now() / 1000); 167 | const initialEndTime = currentTime + SUBSCRIPTION_PERIOD; 168 | // Create initial subscription 169 | const createSubTx = yield program.methods 170 | .subscribe("https://your-cool-agent.com:3001", new anchor.BN(initialEndTime)) 171 | .accounts({ 172 | state: statePDA, 173 | subscriber: subscriber.publicKey, 174 | dataProvider: dataProvider.publicKey, 175 | subscription: subscriptionPDA, 176 | subscribersList: subscribersListPDA, 177 | owner: owner.publicKey, 178 | systemProgram: web3_js_1.SystemProgram.programId, 179 | tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, 180 | nftTokenAccount: providerTokenAccount, 181 | }) 182 | .signers([subscriber]) 183 | .rpc(); 184 | // Confirm the subscription creation 185 | let latestBlockhash = yield provider.connection.getLatestBlockhash(); 186 | let confirmationStrategy = { 187 | signature: createSubTx, 188 | blockhash: latestBlockhash.blockhash, 189 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 190 | }; 191 | yield provider.connection.confirmTransaction(confirmationStrategy); 192 | console.log("Initial subscription created successfully"); 193 | // Now initialize the quality info account 194 | console.log("Initializing quality info account..."); 195 | const [qualityPDA] = web3_js_1.PublicKey.findProgramAddressSync([Buffer.from("quality"), dataProvider.publicKey.toBuffer()], program.programId); 196 | const initQualityTx = yield program.methods 197 | .initializeQualityInfo() 198 | .accounts({ 199 | qualityInfo: qualityPDA, 200 | dataProvider: dataProvider.publicKey, 201 | payer: subscriber.publicKey, 202 | systemProgram: web3_js_1.SystemProgram.programId, 203 | }) 204 | .signers([subscriber]) 205 | .rpc(); 206 | // Confirm quality info initialization 207 | latestBlockhash = yield provider.connection.getLatestBlockhash(); 208 | confirmationStrategy = { 209 | signature: initQualityTx, 210 | blockhash: latestBlockhash.blockhash, 211 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 212 | }; 213 | yield provider.connection.confirmTransaction(confirmationStrategy); 214 | console.log("Quality info account initialized successfully"); 215 | // Now proceed with the subscription renewal 216 | console.log("Proceeding with subscription renewal..."); 217 | const newEndTime = currentTime + (2 * SUBSCRIPTION_PERIOD); 218 | const newRecipient = "https://your-updated-agent.com:3001"; 219 | const quality = 90; 220 | const renewTx = yield program.methods 221 | .renewSubscription(newRecipient, new anchor.BN(newEndTime), quality) 222 | .accounts({ 223 | state: statePDA, 224 | subscriber: subscriber.publicKey, 225 | dataProvider: dataProvider.publicKey, 226 | subscription: subscriptionPDA, 227 | qualityInfo: qualityPDA, 228 | owner: owner.publicKey, 229 | systemProgram: web3_js_1.SystemProgram.programId, 230 | tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, 231 | nftTokenAccount: providerTokenAccount, 232 | }) 233 | .signers([subscriber]) 234 | .rpc(); 235 | // Confirm the renewal transaction 236 | latestBlockhash = yield provider.connection.getLatestBlockhash(); 237 | confirmationStrategy = { 238 | signature: renewTx, 239 | blockhash: latestBlockhash.blockhash, 240 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 241 | }; 242 | yield provider.connection.confirmTransaction(confirmationStrategy); 243 | // Verify the renewal was successful 244 | const subscription = yield program.account.subscription.fetch(subscriptionPDA); 245 | chai_1.assert.equal(subscription.recipient, newRecipient, "Recipient not updated"); 246 | chai_1.assert.ok(subscription.endTime.eq(new anchor.BN(newEndTime)), "End time not updated"); 247 | console.log("Subscription renewed successfully with quality rating stored"); 248 | } 249 | catch (error) { 250 | console.error("Test failed with error:", error); 251 | if (error.logs) { 252 | console.error("Program logs:", error.logs); 253 | } 254 | throw error; 255 | } 256 | })); 257 | (0, mocha_1.it)("Cancels a subscription", () => __awaiter(void 0, void 0, void 0, function* () { 258 | try { 259 | const quality = 85; 260 | const tx = yield program.methods 261 | .cancelSubscription(quality) 262 | .accounts({ 263 | subscriber: subscriber.publicKey, 264 | dataProvider: dataProvider.publicKey, 265 | subscription: subscriptionPDA, 266 | qualityInfo: qualityPDA, 267 | tokenProgram: spl_token_1.TOKEN_PROGRAM_ID, 268 | nftTokenAccount: providerTokenAccount, 269 | }) 270 | .signers([subscriber]) 271 | .rpc(); 272 | // Verify cancellation 273 | const subscription = yield program.account.subscription.fetch(subscriptionPDA); 274 | chai_1.assert.equal(subscription.recipient, "", "Recipient not cleared"); 275 | chai_1.assert.ok(subscription.endTime.eq(new anchor.BN(0)), "End time not cleared"); 276 | console.log("Subscription cancelled successfully"); 277 | } 278 | catch (error) { 279 | console.error("Subscription cancellation failed:", error); 280 | throw error; 281 | } 282 | })); 283 | // Test error conditions 284 | (0, mocha_1.describe)("Error cases", () => { 285 | // it("Prevents subscription with invalid NFT", async () => { 286 | // const invalidProvider = Keypair.generate(); 287 | // const currentTime = Math.floor(Date.now() / 1000); 288 | // const endTime = currentTime + SUBSCRIPTION_PERIOD; 289 | // try { 290 | // await program.methods 291 | // .subscribe("https://your-cool-agent.com:3001", new anchor.BN(endTime)) 292 | // .accounts({ 293 | // state: statePDA, 294 | // subscriber: subscriber.publicKey, 295 | // dataProvider: invalidProvider.publicKey, 296 | // subscription: subscriptionPDA, 297 | // subscribersList: subscribersListPDA, 298 | // owner: owner.publicKey, 299 | // systemProgram: SystemProgram.programId, 300 | // tokenProgram: TOKEN_PROGRAM_ID, 301 | // nftTokenAccount: providerTokenAccount, 302 | // }) 303 | // .signers([subscriber]) 304 | // .rpc(); 305 | // assert.fail("Should have thrown error for invalid NFT"); 306 | // } catch (error) { 307 | // assert.include(error.toString(), "InvalidNFTHolder"); 308 | // } 309 | // }); 310 | (0, mocha_1.it)("Prevents invalid quality ratings", () => __awaiter(void 0, void 0, void 0, function* () { 311 | try { 312 | yield program.methods 313 | .storeDataQuality(101) // Invalid quality > 100 314 | .accounts({ 315 | subscriber: subscriber.publicKey, 316 | dataProvider: dataProvider.publicKey, 317 | qualityInfo: qualityPDA, 318 | }) 319 | .signers([subscriber]) 320 | .rpc(); 321 | chai_1.assert.fail("Should have thrown error for invalid quality"); 322 | } 323 | catch (error) { 324 | chai_1.assert.include(error.toString(), "QualityOutOfRange"); 325 | } 326 | })); 327 | }); 328 | }); 329 | --------------------------------------------------------------------------------