From 1c6fc5540be7dc6e9a75198251e840ed276de43a Mon Sep 17 00:00:00 2001 From: Laura Abro Date: Thu, 24 Apr 2025 10:24:42 -0300 Subject: [PATCH] transfer from monorepo --- .gitignore | 47 + .prettierrc | 23 + planner/placeholder.txt | 0 worker/.env.developer.example | 9 + worker/.env.example | 1 + worker/.gitignore | 17 + worker/README.md | 26 + worker/babel.config.cjs | 1 + worker/config-task-test.yml | 130 ++ worker/config-task.yml | 130 ++ worker/jest.config.js | 7 + worker/orca-agent/.dockerignore | 16 + worker/orca-agent/.env.example | 26 + worker/orca-agent/.gitignore | 1 + worker/orca-agent/Dockerfile | 48 + worker/orca-agent/README.md | 0 worker/orca-agent/main.py | 6 + worker/orca-agent/requirements.txt | 18 + worker/orca-agent/setup.md | 118 ++ worker/orca-agent/setup.py | 8 + worker/orca-agent/src/dababase/models.py | 22 + worker/orca-agent/src/server/__init__.py | 70 + worker/orca-agent/src/server/models/Log.py | 65 + worker/orca-agent/src/server/routes/audit.py | 62 + .../orca-agent/src/server/routes/healthz.py | 14 + .../src/server/routes/repo_summary.py | 54 + worker/orca-agent/src/server/routes/star.py | 39 + .../src/server/routes/submission.py | 38 + .../src/server/services/audit_service.py | 47 + .../src/server/services/github_service.py | 44 + .../server/services/repo_summary_service.py | 60 + .../src/server/services/star_service.py | 50 + worker/orca-agent/src/types.py | 93 ++ .../src/workflows/repoSummarizer/__main__.py | 52 + .../src/workflows/repoSummarizer/phases.py | 67 + .../src/workflows/repoSummarizer/prompts.py | 593 ++++++++ .../src/workflows/repoSummarizer/workflow.py | 277 ++++ .../workflows/repoSummarizerAudit/__main__.py | 52 + .../workflows/repoSummarizerAudit/phases.py | 15 + .../workflows/repoSummarizerAudit/prompts.py | 29 + .../workflows/repoSummarizerAudit/workflow.py | 163 +++ .../src/workflows/starRepo/__main__.py | 57 + .../src/workflows/starRepo/phases.py | 15 + .../src/workflows/starRepo/prompts.py | 29 + .../src/workflows/starRepo/workflow.py | 141 ++ .../src/workflows/starRepoAudit/__main__.py | 58 + .../src/workflows/starRepoAudit/phases.py | 15 + .../src/workflows/starRepoAudit/prompts.py | 29 + .../src/workflows/starRepoAudit/workflow.py | 151 ++ worker/package.json | 76 ++ worker/src/index.ts | 20 + worker/src/orcaSettings.ts | 46 + worker/src/task/0-setup.ts | 4 + worker/src/task/1-task.ts | 215 +++ worker/src/task/2-submission.ts | 102 ++ worker/src/task/3-audit.ts | 84 ++ worker/src/task/4-distribution.ts | 64 + worker/src/task/5-routes.ts | 57 + worker/src/utils/anthropicCheck.ts | 36 + worker/src/utils/constant.ts | 57 + worker/src/utils/distributionList.ts | 96 ++ worker/src/utils/existingIssues.ts | 161 +++ worker/src/utils/githubCheck.ts | 36 + worker/src/utils/ipfs.ts | 34 + worker/src/utils/leader.ts | 265 ++++ .../utils/submissionJSONSignatureDecode.ts | 40 + worker/tests/README.md | 68 + worker/tests/config.ts | 12 + worker/tests/config.yaml | 16 + worker/tests/data/issues.json | 16 + worker/tests/data/todos.json | 20 + worker/tests/debugger.ts | 112 ++ worker/tests/e2e.py | 62 + worker/tests/main.test.ts | 188 +++ worker/tests/prod-debug.ts | 110 ++ worker/tests/simulateTask.ts | 84 ++ worker/tests/stages/__init__.py | 0 worker/tests/stages/audit_summary.py | 51 + worker/tests/stages/fetch_summarizer_todo.py | 39 + worker/tests/stages/generate_summary.py | 47 + worker/tests/stages/submit_summary.py | 56 + worker/tests/stages/validate_api_keys.py | 31 + worker/tests/stages/validate_github.py | 33 + worker/tests/steps.py | 85 ++ worker/tests/test.ts | 19 + worker/tests/wasm/bincode_js.cjs | 1211 +++++++++++++++++ worker/tests/wasm/bincode_js.d.ts | 225 +++ worker/tests/wasm/bincode_js_bg.wasm | Bin 0 -> 192134 bytes worker/tests/wasm/bincode_js_bg.wasm.d.ts | 44 + worker/tests/wasm/zstd.wasm | Bin 0 -> 348579 bytes worker/tests/webpack.config.js | 31 + worker/tests/workers.json | 29 + worker/tsconfig.json | 25 + worker/tsconfig.tests.json | 29 + worker/webpack.config.mjs | 41 + 95 files changed, 7110 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 planner/placeholder.txt create mode 100644 worker/.env.developer.example create mode 100644 worker/.env.example create mode 100644 worker/.gitignore create mode 100644 worker/README.md create mode 100644 worker/babel.config.cjs create mode 100644 worker/config-task-test.yml create mode 100644 worker/config-task.yml create mode 100644 worker/jest.config.js create mode 100644 worker/orca-agent/.dockerignore create mode 100644 worker/orca-agent/.env.example create mode 100644 worker/orca-agent/.gitignore create mode 100644 worker/orca-agent/Dockerfile create mode 100644 worker/orca-agent/README.md create mode 100644 worker/orca-agent/main.py create mode 100644 worker/orca-agent/requirements.txt create mode 100644 worker/orca-agent/setup.md create mode 100644 worker/orca-agent/setup.py create mode 100644 worker/orca-agent/src/dababase/models.py create mode 100644 worker/orca-agent/src/server/__init__.py create mode 100644 worker/orca-agent/src/server/models/Log.py create mode 100644 worker/orca-agent/src/server/routes/audit.py create mode 100644 worker/orca-agent/src/server/routes/healthz.py create mode 100644 worker/orca-agent/src/server/routes/repo_summary.py create mode 100644 worker/orca-agent/src/server/routes/star.py create mode 100644 worker/orca-agent/src/server/routes/submission.py create mode 100644 worker/orca-agent/src/server/services/audit_service.py create mode 100644 worker/orca-agent/src/server/services/github_service.py create mode 100644 worker/orca-agent/src/server/services/repo_summary_service.py create mode 100644 worker/orca-agent/src/server/services/star_service.py create mode 100644 worker/orca-agent/src/types.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizer/__main__.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizer/phases.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizer/prompts.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizer/workflow.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizerAudit/__main__.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizerAudit/phases.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizerAudit/prompts.py create mode 100644 worker/orca-agent/src/workflows/repoSummarizerAudit/workflow.py create mode 100644 worker/orca-agent/src/workflows/starRepo/__main__.py create mode 100644 worker/orca-agent/src/workflows/starRepo/phases.py create mode 100644 worker/orca-agent/src/workflows/starRepo/prompts.py create mode 100644 worker/orca-agent/src/workflows/starRepo/workflow.py create mode 100644 worker/orca-agent/src/workflows/starRepoAudit/__main__.py create mode 100644 worker/orca-agent/src/workflows/starRepoAudit/phases.py create mode 100644 worker/orca-agent/src/workflows/starRepoAudit/prompts.py create mode 100644 worker/orca-agent/src/workflows/starRepoAudit/workflow.py create mode 100644 worker/package.json create mode 100644 worker/src/index.ts create mode 100644 worker/src/orcaSettings.ts create mode 100644 worker/src/task/0-setup.ts create mode 100644 worker/src/task/1-task.ts create mode 100644 worker/src/task/2-submission.ts create mode 100644 worker/src/task/3-audit.ts create mode 100644 worker/src/task/4-distribution.ts create mode 100644 worker/src/task/5-routes.ts create mode 100644 worker/src/utils/anthropicCheck.ts create mode 100644 worker/src/utils/constant.ts create mode 100644 worker/src/utils/distributionList.ts create mode 100644 worker/src/utils/existingIssues.ts create mode 100644 worker/src/utils/githubCheck.ts create mode 100644 worker/src/utils/ipfs.ts create mode 100644 worker/src/utils/leader.ts create mode 100644 worker/src/utils/submissionJSONSignatureDecode.ts create mode 100644 worker/tests/README.md create mode 100644 worker/tests/config.ts create mode 100644 worker/tests/config.yaml create mode 100644 worker/tests/data/issues.json create mode 100644 worker/tests/data/todos.json create mode 100644 worker/tests/debugger.ts create mode 100644 worker/tests/e2e.py create mode 100644 worker/tests/main.test.ts create mode 100644 worker/tests/prod-debug.ts create mode 100644 worker/tests/simulateTask.ts create mode 100644 worker/tests/stages/__init__.py create mode 100644 worker/tests/stages/audit_summary.py create mode 100644 worker/tests/stages/fetch_summarizer_todo.py create mode 100644 worker/tests/stages/generate_summary.py create mode 100644 worker/tests/stages/submit_summary.py create mode 100644 worker/tests/stages/validate_api_keys.py create mode 100644 worker/tests/stages/validate_github.py create mode 100644 worker/tests/steps.py create mode 100644 worker/tests/test.ts create mode 100644 worker/tests/wasm/bincode_js.cjs create mode 100644 worker/tests/wasm/bincode_js.d.ts create mode 100644 worker/tests/wasm/bincode_js_bg.wasm create mode 100644 worker/tests/wasm/bincode_js_bg.wasm.d.ts create mode 100755 worker/tests/wasm/zstd.wasm create mode 100644 worker/tests/webpack.config.js create mode 100644 worker/tests/workers.json create mode 100644 worker/tsconfig.json create mode 100644 worker/tsconfig.tests.json create mode 100644 worker/webpack.config.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b12ec12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +.venv +.env +__pycache__ +.pytest_cache +*.db +test +test_state.json +task_flow.egg-info +example_repo +signature.js +git-filter-repo +task/orca/ +**/dist/ +# yarn.lock +package-lock.json +node_modules +build +migrate.sh +*/dev.js +executables/* +namespace/* +config/* +.env.local +taskStateInfoKeypair.json +localKOIIDB.db +metadata.json +.npmrc +*.pem +.vscode +.cursor +data/chunks +data/process +test_state.csv +todos-example.csv + + +# Ignore auto-generated repository directories +repos/ + + +# Ignore Data +data/* + + +venv + +**/venv/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..86a5cc2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,23 @@ +{ + "useTabs": false, + "tabWidth": 2, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 120, + "arrowParens": "always", + "semi": true, + "overrides": [ + { + "files": ["*.py"], + "options": { + "tabWidth": 4 + } + }, + { + "files": ".eslintrc", + "options": { + "parser": "json" + } + } + ] +} diff --git a/planner/placeholder.txt b/planner/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/worker/.env.developer.example b/worker/.env.developer.example new file mode 100644 index 0000000..e3e1b37 --- /dev/null +++ b/worker/.env.developer.example @@ -0,0 +1,9 @@ +# This File is for prod-debug.js + +TASK_ID='FGzVTXn6iZFhFo9FgWW6zoHfDkJepQkKKKPfMvDdvePv' # Easy Testing Task ID +TEST_KEYWORDS='TEST,EZ TESTING' + +# Set this to use your desktop node staking wallet during testing so IPFS will work +# See https://github.com/koii-network/ezsandbox/blob/main/Lesson%201/PartIV.md#staking-wallet +STAKING_WALLET_PATH="path to your desktop node staking wallet" +MIDDLE_SERVER_URL="http://localhost:3000" diff --git a/worker/.env.example b/worker/.env.example new file mode 100644 index 0000000..4c03b66 --- /dev/null +++ b/worker/.env.example @@ -0,0 +1 @@ +DEFAULT_BOUNTY_MARKDOWN_FILE=https://raw.githubusercontent.com/HermanL02/prometheus-swarm-bounties/master/README.md # Testing only \ No newline at end of file diff --git a/worker/.gitignore b/worker/.gitignore new file mode 100644 index 0000000..6aee1de --- /dev/null +++ b/worker/.gitignore @@ -0,0 +1,17 @@ +dist +build +node_modules +package-lock.json +yarn.lock +migrate.sh +*/dev.js +data/* +executables/* +namespace/* +config/* +.env.local +.env +taskStateInfoKeypair.json +localKOIIDB.db +metadata.json +.npmrc diff --git a/worker/README.md b/worker/README.md new file mode 100644 index 0000000..d24385c --- /dev/null +++ b/worker/README.md @@ -0,0 +1,26 @@ +# Earn Crypto with AI Agents: Prometheus Document & Summarize Task (Beta v0) + +## Overview + +The **Prometheus Document & Summarize Task** spins up an **AI agent** capable of continuously summarizing repositories, **earning you KOII**. Automated document summarization agents can constantly process and summarize information, increasing the value of the network _and_ your node. Our ultimate goal is to have **AI agents summarizing Koii tasks**, growing the network with **more opportunities for node operators to earn rewards**. + +## Releases + +### Beta v0 + +- This is the **first beta release** of the task. +- The AI agent reads documents and generates summaries automatically. +- Documentations are sent to the user repository. +- Future versions will introduce **enhanced AI logic, more complex summarization tasks, and more!** + +## Task Setup + +**[How to set up a Claude API key and a GitHub API key for the 247 Document & Summarize Task.](https://www.koii.network/blog/Earn-Crypto-With-AI-Agent)** + +## How It Works + +1. The Koii Node **launches an AI agent** inside a lightweight runtime. +2. The agent reads an active **repository list** from the bounty repository. +3. It picks a **repository**, generates the necessary **documentation**, and submits a **Github pull request** (a request to have its documentation added to the repository). +4. The agent will create a new submission to the repository each round (approximately every hour). +5. Koii Nodes **earn rewards** for running the AI agent and contributing documentation. \ No newline at end of file diff --git a/worker/babel.config.cjs b/worker/babel.config.cjs new file mode 100644 index 0000000..7bfb4c1 --- /dev/null +++ b/worker/babel.config.cjs @@ -0,0 +1 @@ +module.exports = { presets: ["@babel/preset-env", "@babel/preset-typescript"] }; diff --git a/worker/config-task-test.yml b/worker/config-task-test.yml new file mode 100644 index 0000000..c50d18b --- /dev/null +++ b/worker/config-task-test.yml @@ -0,0 +1,130 @@ +######################## ALL FIELDS ARE REQUIRED UNLESS OTHERWISE NOTED ######################### + +######################################### TASK METADATA ######################################### +############################ Will be displayed in the desktop node ############################## + +## Task Name ## +# Maximum 24 characters. +task_name: "Prometheus Docs Agent" + +## Task Author ## +author: "Prometheus" + +# Task Description Markdown ## +# If you specify a markdown file, the description field will be ignored. +# Markdown is recommended for better formatting. +markdownDescriptionPath: "./README.md" + +## Task Description ## +# Ignored if you specify a markdown file. +description: "Task description." + +## Repository URL ## +# Must be public for whitelisted tasks. +repositoryUrl: "https://github.com/koii-network/builder-247" + +## Image URL ## +# 230x86 pixels. +imageUrl: "https://koii-k2-task-metadata.s3.us-east-2.amazonaws.com/Docs.png" + +## Info URL ## +infoUrl: "https://www.koii.network/blog/Earn-Crypto-With-AI-Agent" + +####################################### TASK CONFIGURATION ###################################### + +## Task Executable Network ## +# IPFS or DEVELOPMENT +# Keep this as IPFS unless you know you need to change it. +task_executable_network: "IPFS" + +## Task Audit Program ## +# Task Executable Network IPFS: Path to your executable. +# Task Executable Network DEVELOPMENT: The value should be 'main'. +# Keep this as-is unless you know you need to change it. +task_audit_program: "dist/main.js" + +## Round Time ## +# Duration of task, measured in slots (with each slot approximately equal to 408ms). Should be at least 800 slots. +# See https://www.koii.network/docs/concepts/what-are-tasks/what-are-tasks/gradual-consensus for more information on how round time, audit window, and submission window work. +round_time: 1500 + +## Audit Window ## +# The audit window should be at least 1/3 of the round time. +audit_window: 600 + +## Submission Window ## +# The submission window should be at least 1/3 of the round time. +submission_window: 600 + +## Minimum Stake Amount ## +# The minimum amount of KOII or KPL that a user must stake in order to participate in the task. +minimum_stake_amount: 0.01 + +## Task Bounty Type ## +# KOII or KPL +task_type: "KOII" + +## Token Mint Address (ONLY for KPL tasks) ## +# The Fire Token address is provided as an example. +token_type: "4qayyw53kWz6GzypcejjT1cvwMXS1qYLSMQRE8se3gTv" + +## Total Bounty Amount ## +# The total bounty amount that will be available for distribution over all rounds. +# Does nothing when updating a task. +total_bounty_amount: 11 + +## Bounty Amount per Round ## +# The maximum amount that can be distributed per round. +# If the actual distribution per round exceeds this amount, the distribution list will fail. +bounty_amount_per_round: 1 + +## Allowed Failed Distributions ## +# Number of retries allowed for the distribution list if it is fails audit. +# If all retries fail, the task will not distribute anything for the round. +# This is also the number of rounds of submissions it will keep. +allowed_failed_distributions: 8 + +## Space ## +# Expected Task Data Size in MBs for the account size. +# Minimums: 2 for whitelisted tasks, 1 for production, 0.1 for testing. +# See https://www.koii.network/docs/develop/command-line-tool/create-task-cli/create-task#space for calculation details. +space: 0.1 + +## Requirement Tags (Optional) ## +# To add more global variables and task variables, please refer to the type, value, description format shown below. +# The ORCA_TASK addon is REQUIRED +requirementsTags: + - type: ADDON + value: "ORCA_TASK" + - type: CPU + value: "4-core" + - type: RAM + value: "5 GB" + - type: STORAGE + value: "5 GB" + - type: TASK_VARIABLE + value: "ANTHROPIC_API_KEY" + description: "Your Anthropic API key. You can get one here: https://console.anthropic.com/settings/keys" + - type: TASK_VARIABLE + value: "GITHUB_USERNAME" + description: "Your GitHub username. You can sign up for an account here: https://github.com/join" + - type: TASK_VARIABLE + value: "GITHUB_TOKEN" + description: "Your GitHub Personal Access Token. You can create one here: https://github.com/settings/tokens" + +## Tags ## +# See https://www.koii.network/docs/develop/command-line-tool/create-task-cli/create-task#tags for available tag options. +tags: ["AI"] + +# Environment ## +# TEST or PRODUCTION +# Production mode will expose your task to all the task runners, even if not whitelisted. +environment: "TEST" + +#################################### FOR UPDATING TASKS ONLY #################################### + +## Old Task ID ## +task_id: "48h3f4r3AR7MdgCMkET4v3yh7PpPHuqGDWzqgH52rny1" + +## Migration Description ## +migrationDescription: "Fix audit bug" diff --git a/worker/config-task.yml b/worker/config-task.yml new file mode 100644 index 0000000..551a730 --- /dev/null +++ b/worker/config-task.yml @@ -0,0 +1,130 @@ +######################## ALL FIELDS ARE REQUIRED UNLESS OTHERWISE NOTED ######################### + +######################################### TASK METADATA ######################################### +############################ Will be displayed in the desktop node ############################## + +## Task Name ## +# Maximum 24 characters. +task_name: "Prometheus Docs Agent" + +## Task Author ## +author: "Prometheus" + +# Task Description Markdown ## +# If you specify a markdown file, the description field will be ignored. +# Markdown is recommended for better formatting. +markdownDescriptionPath: "./README.md" + +## Task Description ## +# Ignored if you specify a markdown file. +description: "Task description." + +## Repository URL ## +# Must be public for whitelisted tasks. +repositoryUrl: "https://github.com/koii-network/builder-247" + +## Image URL ## +# 230x86 pixels. +imageUrl: "https://koii-k2-task-metadata.s3.us-east-2.amazonaws.com/Docs.png" + +## Info URL ## +infoUrl: "https://www.koii.network/blog/Earn-Crypto-With-AI-Agent" + +####################################### TASK CONFIGURATION ###################################### + +## Task Executable Network ## +# IPFS or DEVELOPMENT +# Keep this as IPFS unless you know you need to change it. +task_executable_network: "IPFS" + +## Task Audit Program ## +# Task Executable Network IPFS: Path to your executable. +# Task Executable Network DEVELOPMENT: The value should be 'main'. +# Keep this as-is unless you know you need to change it. +task_audit_program: "dist/main.js" + +## Round Time ## +# Duration of task, measured in slots (with each slot approximately equal to 408ms). Should be at least 800 slots. +# See https://www.koii.network/docs/concepts/what-are-tasks/what-are-tasks/gradual-consensus for more information on how round time, audit window, and submission window work. +round_time: 5000 + +## Audit Window ## +# The audit window should be at least 1/3 of the round time. +audit_window: 2200 + +## Submission Window ## +# The submission window should be at least 1/3 of the round time. +submission_window: 2200 + +## Minimum Stake Amount ## +# The minimum amount of KOII or KPL that a user must stake in order to participate in the task. +minimum_stake_amount: 0.01 + +## Task Bounty Type ## +# KOII or KPL +task_type: "KOII" + +## Token Mint Address (ONLY for KPL tasks) ## +# The Fire Token address is provided as an example. +token_type: "4qayyw53kWz6GzypcejjT1cvwMXS1qYLSMQRE8se3gTv" + +## Total Bounty Amount ## +# The total bounty amount that will be available for distribution over all rounds. +# Does nothing when updating a task. +total_bounty_amount: 12000 + +## Bounty Amount per Round ## +# The maximum amount that can be distributed per round. +# If the actual distribution per round exceeds this amount, the distribution list will fail. +bounty_amount_per_round: 2000 + +## Allowed Failed Distributions ## +# Number of retries allowed for the distribution list if it is fails audit. +# If all retries fail, the task will not distribute anything for the round. +# This is also the number of rounds of submissions it will keep. +allowed_failed_distributions: 8 + +## Space ## +# Expected Task Data Size in MBs for the account size. +# Minimums: 2 for whitelisted tasks, 1 for production, 0.1 for testing. +# See https://www.koii.network/docs/develop/command-line-tool/create-task-cli/create-task#space for calculation details. +space: 5 + +## Requirement Tags (Optional) ## +# To add more global variables and task variables, please refer to the type, value, description format shown below. +# The ORCA_TASK addon is REQUIRED +requirementsTags: + - type: ADDON + value: "ORCA_TASK" + - type: CPU + value: "4-core" + - type: RAM + value: "5 GB" + - type: STORAGE + value: "5 GB" + - type: TASK_VARIABLE + value: "ANTHROPIC_API_KEY" + description: "Your Anthropic API key. You can get one here: https://console.anthropic.com/settings/keys" + - type: TASK_VARIABLE + value: "GITHUB_USERNAME" + description: "Your GitHub username. You can sign up for an account here: https://github.com/join" + - type: TASK_VARIABLE + value: "GITHUB_TOKEN" + description: "Your GitHub Personal Access Token. You can create one here: https://github.com/settings/tokens" + +## Tags ## +# See https://www.koii.network/docs/develop/command-line-tool/create-task-cli/create-task#tags for available tag options. +tags: ["AI"] + +# Environment ## +# TEST or PRODUCTION +# Production mode will expose your task to all the task runners, even if not whitelisted. +environment: "TEST" + +#################################### FOR UPDATING TASKS ONLY #################################### + +## Old Task ID ## +task_id: "62n2aAVVV42rtt53wxieotTdnKpTRjiChsHYdSxHDhAZ" + +## Migration Description ## +migrationDescription: "Error logging and Bug Fixing and Slack Notification" diff --git a/worker/jest.config.js b/worker/jest.config.js new file mode 100644 index 0000000..ff3592e --- /dev/null +++ b/worker/jest.config.js @@ -0,0 +1,7 @@ +export default { + transform: { "^.+\\.tsx?$": "babel-jest" }, + transformIgnorePatterns: ["/node_modules/(?!@babel/runtime)"], + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testEnvironment: "node", + }; + \ No newline at end of file diff --git a/worker/orca-agent/.dockerignore b/worker/orca-agent/.dockerignore new file mode 100644 index 0000000..6d9202f --- /dev/null +++ b/worker/orca-agent/.dockerignore @@ -0,0 +1,16 @@ +**/.env +**/.env.* + +**/node_modules +**/dist +**/build +**/*.log +**/Dockerfile +**/docker-compose.yml +**/venv +**/.venv +**/*__pycache__ +**/.pytest_cache +**/*.db +**/*.egg-info +**/*/repos/ diff --git a/worker/orca-agent/.env.example b/worker/orca-agent/.env.example new file mode 100644 index 0000000..8757f40 --- /dev/null +++ b/worker/orca-agent/.env.example @@ -0,0 +1,26 @@ +ANTHROPIC_API_KEY=your_anthropic_api_key +# the token requires the repo scope +GITHUB_TOKEN=your_github_token +GITHUB_USERNAME=your_github_username + +# for testing only +# these credentials must be different from the ones above +# they are used to create and delete test repositories +# the token requires the repo and delete_repo scopes +UPSTREAM_GITHUB_TOKEN=your_upstream_github_token +UPSTREAM_GITHUB_USERNAME=your_upstream_github_username + +# for testing only +MIDDLE_SERVER_URL=http://localhost:3000 + +TASK_SYSTEM_PROMPT="You are an AI development assistant specializing in writing code and creating GitHub pull requests. +Follow these rules: +1. Create a new file in the /src directory. +2. Write a single Python function that accomplishes the assigned task. +3. Commit and push the changes to the remote repository. +4. Create a second new file in the /tests directory. +5. Write a series of tests that thoroughly test the function, including edge cases and error handling, using PyTest. +6. Commit and push the changes to the remote repository. +7. Run the tests to ensure they pass. +8. Continue to make commits and push them to the remote repository until the tests pass. +9. Validate code changes before submitting" diff --git a/worker/orca-agent/.gitignore b/worker/orca-agent/.gitignore new file mode 100644 index 0000000..5ceb386 --- /dev/null +++ b/worker/orca-agent/.gitignore @@ -0,0 +1 @@ +venv diff --git a/worker/orca-agent/Dockerfile b/worker/orca-agent/Dockerfile new file mode 100644 index 0000000..c3772b9 --- /dev/null +++ b/worker/orca-agent/Dockerfile @@ -0,0 +1,48 @@ +# Use the official Python image from the Docker Hub +FROM python:3.12-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the requirements.txt file into the container +COPY requirements.txt . + + +# Install Git and any other necessary packages +RUN apt-get update && apt-get install -y git sudo curl + +# Install the dependencies +RUN pip install -r requirements.txt + +# Configure Git to add the safe directory +RUN git config --global --add safe.directory /app + +# Copy the rest of your application code into the container +COPY . . + +ENV MIDDLE_SERVER_URL=https://builder247.api.koii.network + +# Configure logging and output +ENV PYTHONUNBUFFERED=1 +ENV TERM=xterm-256color +ENV FORCE_COLOR=1 + +# Add this environment variable after other ENV declarations +ENV DATABASE_PATH=/data/database.db + +# Make port 8080 available to the world outside this container +EXPOSE 8080 + +# Set the command to run your application +CMD ["gunicorn", \ + "--log-level=error", \ + "--error-logfile=-", \ + "--capture-output", \ + "--enable-stdio-inheritance", \ + "--logger-class=gunicorn.glogging.Logger", \ + "--timeout", "600", \ + "--graceful-timeout", "600", \ + "--keep-alive", "5", \ + "-w", "1", \ + "-b", "0.0.0.0:8080", \ + "main:app"] diff --git a/worker/orca-agent/README.md b/worker/orca-agent/README.md new file mode 100644 index 0000000..e69de29 diff --git a/worker/orca-agent/main.py b/worker/orca-agent/main.py new file mode 100644 index 0000000..59164d4 --- /dev/null +++ b/worker/orca-agent/main.py @@ -0,0 +1,6 @@ +from src.server import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080, debug=True) diff --git a/worker/orca-agent/requirements.txt b/worker/orca-agent/requirements.txt new file mode 100644 index 0000000..14fdcc8 --- /dev/null +++ b/worker/orca-agent/requirements.txt @@ -0,0 +1,18 @@ +anthropic>=0.8.1 +python-dotenv>=1.0.0 +pandas>=2.0.0 +tiktoken>=0.5.2 +pytest>=8.0.2 +typing-extensions>=4.12.2 +GitPython>=3.1.44 +pygithub>=2.5.0 +Flask>=3.0.0 +requests>=2.32.0 +cryptography>=42.0.0 +gunicorn>=22.0.0 +solders>=0.26.0 +base58>=2.1.0 +tenacity>=9.0.0 +sqlmodel>=0.0.22 +openai>=0.28.0 +colorama>=0.4.6 diff --git a/worker/orca-agent/setup.md b/worker/orca-agent/setup.md new file mode 100644 index 0000000..602f9b6 --- /dev/null +++ b/worker/orca-agent/setup.md @@ -0,0 +1,118 @@ +# 247 Builder + +## Developing locally + +Navigate to the correct directory: + +```sh +cd builder/container +``` + +Set up a virtual environment and activate it: + +```sh +python3 -m venv .venv +source .venv/bin/activate +``` + +Install dependencies: + +```sh +pip install -r requirements.txt +``` + +Run tests: + +```sh +python3 -m pytest tests/ +``` + +Run the agent: + +```sh +python3 main.py +``` + +## Developing in Docker + +### Running the Flask Server + +Navigate to the correct directory: + +```sh +cd builder/container +``` + +Build the image: + +```sh +docker build -t builder247 . +``` + +Run the container: + +```sh +docker run builder247 +``` + +You can also run with a mounted volume if you'd like to change files without updating the container: + +```sh +docker run -v $(pwd):/app builder247 +``` + +### Running Interactively (using the shell) + +Navigate to the correct directory: + +```sh +cd builder/container +``` + +Change this line in the Dockerfile: + +```sh +CMD ["python", "main.py"] +``` + +to + +```sh +CMD ["/bin/bash"] +``` + +Build the image: + +```sh +docker build -t builder247. +``` + +Run the container with a mounted volume: + +```sh +docker run -it -v $(pwd)/builder:/app builder247 +``` + +This will give you access to your files within the container and run the container in interactive mode with shell access. You can then run tests inside the container using: + +```sh +python -m pytest tests/ +``` + +or + +```sh +python3 -m pytest tests/ +``` + +You can also run the flask server in the container with: + +```sh +python main.py +``` + +To exit the container's shell: + +```sh +exit +``` diff --git a/worker/orca-agent/setup.py b/worker/orca-agent/setup.py new file mode 100644 index 0000000..1e17926 --- /dev/null +++ b/worker/orca-agent/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, find_packages + +setup( + name="task-flow", + version="0.1", + packages=find_packages(include=["src", "src.*"]), + python_requires=">=3.6", +) diff --git a/worker/orca-agent/src/dababase/models.py b/worker/orca-agent/src/dababase/models.py new file mode 100644 index 0000000..c361904 --- /dev/null +++ b/worker/orca-agent/src/dababase/models.py @@ -0,0 +1,22 @@ +"""Database models.""" + +from datetime import datetime +from typing import Optional, List +from sqlmodel import SQLModel, Field, Relationship +from sqlalchemy import JSON +from sqlalchemy import Column +from prometheus_swarm.database.models import Conversation, Message, Log + + +class Submission(SQLModel, table=True): + """Task submission model.""" + + task_id: str + round_number: int = Field(primary_key=True) + status: str = "pending" + pr_url: Optional[str] = None + username: Optional[str] = None + repo_urls: Optional[dict] = Field( + default=None, sa_column=Column(JSON) + ) # Store as JSON type + repo_url: Optional[str] = None \ No newline at end of file diff --git a/worker/orca-agent/src/server/__init__.py b/worker/orca-agent/src/server/__init__.py new file mode 100644 index 0000000..defdb07 --- /dev/null +++ b/worker/orca-agent/src/server/__init__.py @@ -0,0 +1,70 @@ +"""Flask application initialization.""" + +from flask import Flask, request +from .routes import repo_summary, star, audit, healthz, submission +from prometheus_swarm.utils.logging import configure_logging, log_section, log_key_value, log_value +from prometheus_swarm.database import initialize_database +from colorama import Fore, Style +import uuid +import os + + +def create_app(): + """Create and configure the Flask application.""" + app = Flask(__name__) + + # Add request ID middleware + @app.before_request + def before_request(): + request.id = str(uuid.uuid4()) + # Store request start time for duration calculation + request.start_time = request.environ.get("REQUEST_TIME", 0) + + @app.after_request + def after_request(response): + # Calculate request duration + duration = (request.environ.get("REQUEST_TIME", 0) - request.start_time) * 1000 + + # Get error message if this is an error response + error_msg = "" + if response.status_code >= 400: + try: + json_data = response.get_json() + if isinstance(json_data, dict): + error_msg = json_data.get("error") or json_data.get("message", "") + except Exception: + # If we can't get JSON data, try to get the message from the response + error_msg = getattr(response, "description", "") + + # Log the request with appropriate color + color = Fore.GREEN if response.status_code < 400 else Fore.RED + log_value( + f"[{color}REQ{Style.RESET_ALL}] {request.method} {request.path} " + f"{color}{response.status_code}{Style.RESET_ALL} {error_msg} {duration}ms" + ) + + return response + + # Register blueprints + app.register_blueprint(healthz.bp) + app.register_blueprint(repo_summary.bp) + app.register_blueprint(star.bp) + app.register_blueprint(audit.bp) + app.register_blueprint(submission.bp) + + # Configure logging within app context + with app.app_context(): + # Set up logging (includes both console and database logging) + configure_logging() + # Initialize database + initialize_database() + # Disable Flask's default logging + app.logger.disabled = True + + # Log startup information + log_section("SERVER STARTUP") + log_key_value("Workers", 1) + log_key_value("Host", "0.0.0.0:8080") + log_key_value("Database", os.getenv("DATABASE_PATH", "Not configured")) + + return app diff --git a/worker/orca-agent/src/server/models/Log.py b/worker/orca-agent/src/server/models/Log.py new file mode 100644 index 0000000..6394a26 --- /dev/null +++ b/worker/orca-agent/src/server/models/Log.py @@ -0,0 +1,65 @@ +"""Database model for logging.""" + +from datetime import datetime +from prometheus_swarm.database import get_db + + +def init_logs_table(): + """Initialize the logs table if it doesn't exist.""" + # Not needed - handled by SQLModel + pass + + +def save_log( + level: str, + message: str, + module: str = None, + function: str = None, + path: str = None, + line_no: int = None, + exception: str = None, + stack_trace: str = None, + request_id: str = None, + additional_data: str = None, +) -> bool: + """ + Save a log entry to the database. + + Args: + level: Log level (ERROR, WARNING, INFO, etc) + message: Log message + module: Module name where log was generated + function: Function name where log was generated + path: File path where log was generated + line_no: Line number where log was generated + exception: Exception type if any + stack_trace: Stack trace if any + request_id: Request ID if available + additional_data: Any additional JSON-serializable data + + Returns: + bool: True if log was saved successfully + """ + try: + db = get_db() + from prometheus_swarm.database import Log + + log = Log( + timestamp=datetime.utcnow(), + level=level, + message=message, + module=module, + function=function, + path=path, + line_no=line_no, + exception=exception, + stack_trace=stack_trace, + request_id=request_id, + additional_data=additional_data, + ) + db.add(log) + db.commit() + return True + except Exception as e: + print(f"Failed to save log to database: {e}") # Fallback logging + return False diff --git a/worker/orca-agent/src/server/routes/audit.py b/worker/orca-agent/src/server/routes/audit.py new file mode 100644 index 0000000..0757a29 --- /dev/null +++ b/worker/orca-agent/src/server/routes/audit.py @@ -0,0 +1,62 @@ +from flask import Blueprint, jsonify, request +from src.server.services.github_service import verify_pr_ownership +from src.server.services.audit_service import audit_repo +import logging + +logger = logging.getLogger(__name__) + +bp = Blueprint("audit", __name__) + + +@bp.post("/audit/") +def audit_submission(round_number: int): + logger.info("Auditing submission") + + data = request.get_json() + submission = data.get("submission") + + if not submission: + return jsonify({"error": "Missing submission"}), 400 + + # submission_round_number = submission.get("roundNumber") + task_id = submission.get("taskId") + pr_url = submission.get("prUrl") + github_username = submission.get("githubUsername") + + # Extract repo owner and name from PR URL + try: + pr_url_parts = pr_url.split('github.com/')[1].split('/') + repo_owner = pr_url_parts[0] + repo_name = pr_url_parts[1] + except (IndexError, AttributeError): + return jsonify({"error": "Invalid PR URL format"}), 400 + print(f"Repo owner: {repo_owner}, Repo name: {repo_name}") + # This is commented out because the round number might be different due to we put the audit logic in the distribution part + # if int(round_number) != submission_round_number: + # return jsonify({"error": "Round number mismatch"}), 400 + + if ( + not task_id + or not pr_url + or not github_username + or not repo_owner + or not repo_name + ): + return jsonify({"error": "Missing submission data"}), 400 + + is_valid = verify_pr_ownership( + pr_url=pr_url, + expected_username=github_username, + expected_owner=repo_owner, + expected_repo=repo_name, + ) + + if not is_valid: + return jsonify(False) + + try: + is_approved = audit_repo(pr_url) + return jsonify(is_approved), 200 + except Exception as e: + logger.error(f"Error auditing PR: {str(e)}") + return jsonify(True), 200 diff --git a/worker/orca-agent/src/server/routes/healthz.py b/worker/orca-agent/src/server/routes/healthz.py new file mode 100644 index 0000000..21f122c --- /dev/null +++ b/worker/orca-agent/src/server/routes/healthz.py @@ -0,0 +1,14 @@ +from flask import Blueprint, jsonify +from prometheus_swarm.database import get_db +import logging + +logger = logging.getLogger(__name__) + +bp = Blueprint("healthz", __name__) + + +@bp.post("/healthz") +def healthz(): + # Test database connection + _ = get_db() + return jsonify({"status": "ok"}) diff --git a/worker/orca-agent/src/server/routes/repo_summary.py b/worker/orca-agent/src/server/routes/repo_summary.py new file mode 100644 index 0000000..2c3df4d --- /dev/null +++ b/worker/orca-agent/src/server/routes/repo_summary.py @@ -0,0 +1,54 @@ +from flask import Blueprint, jsonify, request +from src.server.services import repo_summary_service + +bp = Blueprint("repo_summary", __name__) + + +@bp.post("/repo_summary/") +def start_task(round_number): + logger = repo_summary_service.logger + logger.info(f"Task started for round: {round_number}") + + data = request.get_json() + logger.info(f"Task data: {data}") + required_fields = [ + "taskId", + "round_number", + "repo_url" + ] + if any(data.get(field) is None for field in required_fields): + return jsonify({"error": "Missing data"}), 401 + + result = repo_summary_service.handle_task_creation( + task_id=data["taskId"], + round_number=int(round_number), + repo_url=data["repo_url"], + ) + + return result + +if __name__ == "__main__": + from flask import Flask + + # Create a Flask app instance + app = Flask(__name__) + app.register_blueprint(bp) + + # Test data + test_data = { + "taskId": "fake", + "round_number": "1", + "repo_url": "https://github.com/koii-network/docs" + } + + # Set up test context + with app.test_client() as client: + # Make a POST request to the endpoint + response = client.post( + "/repo_summary/1", + json=test_data + ) + + # Print the response + print(f"Status Code: {response.status_code}") + print(f"Response: {response.get_json()}") diff --git a/worker/orca-agent/src/server/routes/star.py b/worker/orca-agent/src/server/routes/star.py new file mode 100644 index 0000000..68eb042 --- /dev/null +++ b/worker/orca-agent/src/server/routes/star.py @@ -0,0 +1,39 @@ +from prometheus_swarm.utils.logging import log_key_value +from flask import Blueprint, jsonify, request +from src.server.services import star_service + +bp = Blueprint("star", __name__) + + +@bp.post("/star/") +def start_task(round_number): + logger = star_service.logger + logger.info(f"Task started for round: {round_number}") + + data = request.get_json() + logger.info(f"Task data: {data}") + required_fields = [ + "taskId", + "round_number", + "github_urls", + ] + if any(data.get(field) is None for field in required_fields): + return jsonify({"error": "Missing data"}), 401 + + try: + # Log incoming data + print("Received data:", data) + print("Round number:", round_number) + + result = star_service.handle_star_task( + task_id=data["taskId"], + round_number=int(round_number), + github_urls=data["github_urls"], + ) + return result + except Exception as e: + print(f"Error in star endpoint: {str(e)}") + print(f"Error type: {type(e)}") + import traceback + print(f"Traceback: {traceback.format_exc()}") + return jsonify({'error': str(e)}), 500 diff --git a/worker/orca-agent/src/server/routes/submission.py b/worker/orca-agent/src/server/routes/submission.py new file mode 100644 index 0000000..a4efa34 --- /dev/null +++ b/worker/orca-agent/src/server/routes/submission.py @@ -0,0 +1,38 @@ +from flask import Blueprint, jsonify +from prometheus_swarm.database import get_db +from src.dababase.models import Submission +import logging +import os + +logger = logging.getLogger(__name__) + +bp = Blueprint("submission", __name__) + + +@bp.get("/submission/") +def fetch_submission(roundNumber): + logger.info(f"Fetching submission for round: {roundNumber}") + db = get_db() + submission = ( + db.query(Submission) + .filter( + Submission.round_number == int(roundNumber), + ) + .first() + ) + logger.info(f"Submission: {submission}") + logger.info(f"Submission: {submission}") + if submission: + + github_username = os.getenv("GITHUB_USERNAME") + return jsonify( + { + "taskId": submission.task_id, + "roundNumber": submission.round_number, + "status": submission.status, + "prUrl": submission.pr_url, + "githubUsername": github_username, + } + ) + else: + return jsonify({"error": "Submission not found"}), 409 diff --git a/worker/orca-agent/src/server/services/audit_service.py b/worker/orca-agent/src/server/services/audit_service.py new file mode 100644 index 0000000..025dc4a --- /dev/null +++ b/worker/orca-agent/src/server/services/audit_service.py @@ -0,0 +1,47 @@ +"""Audit service module.""" + +import logging +from prometheus_swarm.clients import setup_client +from src.workflows.repoSummarizerAudit.workflow import repoSummarizerAuditWorkflow +from src.workflows.repoSummarizerAudit.prompts import ( + PROMPTS as REPO_SUMMARIZER_AUDIT_PROMPTS, +) + +logger = logging.getLogger(__name__) + + +def audit_repo(pr_url): + # def review_pr(repo_urls, pr_url, github_username, star_only=True): + """Review PR and decide if it should be accepted, revised, or rejected.""" + try: + # Set up client and workflow + client = setup_client("anthropic") + + # Below commented out because we won't need to distribute starring repo nodes + # star_repo_workflow = StarRepoAuditWorkflow( + # client=client, + # prompts=STAR_REPO_AUDIT_PROMPTS, + # repo_url=repo_urls[0], + # github_username=github_username, + # ) + # star_repo_workflow.run() + + repo_summerizer_audit_workflow = repoSummarizerAuditWorkflow( + client=client, + prompts=REPO_SUMMARIZER_AUDIT_PROMPTS, + pr_url=pr_url, + ) + + # Run workflow and get result + result = repo_summerizer_audit_workflow.run() + recommendation = result["data"]["recommendation"] + return recommendation + except Exception as e: + logger.error(f"PR review failed: {str(e)}") + raise Exception("PR review failed") + + +if __name__ == "__main__": + # review_pr(["https://github.com/alexander-morris/koii-dumper-reveal"], "https://github.com/koii-network/namespace-wrapper/pull/1", "HermanL02") + + audit_repo("https://github.com/koii-network/namespace-wrapper/pull/1") diff --git a/worker/orca-agent/src/server/services/github_service.py b/worker/orca-agent/src/server/services/github_service.py new file mode 100644 index 0000000..17165e9 --- /dev/null +++ b/worker/orca-agent/src/server/services/github_service.py @@ -0,0 +1,44 @@ +import re +import requests +from github import Github +import os +import logging + +logger = logging.getLogger(__name__) + + +def verify_pr_ownership( + pr_url, + expected_username, + expected_owner, + expected_repo, +): + try: + gh = Github(os.environ.get("GITHUB_TOKEN")) + + match = re.match(r"https://github.com/([^/]+)/([^/]+)/pull/(\d+)", pr_url) + if not match: + logger.error(f"Invalid PR URL: {pr_url}") + return False + + owner, repo_name, pr_number = match.groups() + + if owner != expected_owner or repo_name != expected_repo: + logger.error( + f"PR URL mismatch: {pr_url} != {expected_owner}/{expected_repo}" + ) + return False + + repo = gh.get_repo(f"{owner}/{repo_name}") + pr = repo.get_pull(int(pr_number)) + + if pr.user.login != expected_username: + logger.error( + f"PR username mismatch: {pr.user.login} != {expected_username}" + ) + return False + return True + + except Exception as e: + logger.error(f"Error verifying PR ownership: {str(e)}") + return True diff --git a/worker/orca-agent/src/server/services/repo_summary_service.py b/worker/orca-agent/src/server/services/repo_summary_service.py new file mode 100644 index 0000000..6fcc52f --- /dev/null +++ b/worker/orca-agent/src/server/services/repo_summary_service.py @@ -0,0 +1,60 @@ +"""Task service module.""" + +import requests +import os +from flask import jsonify +from prometheus_swarm.database import get_db +from prometheus_swarm.clients import setup_client +from src.workflows.repoSummarizer.workflow import RepoSummarizerWorkflow +from prometheus_swarm.utils.logging import logger, log_error +from dotenv import load_dotenv +from src.workflows.repoSummarizer.prompts import PROMPTS +from src.dababase.models import Submission + +load_dotenv() + + +def handle_task_creation(task_id, round_number, repo_url): + """Handle task creation request.""" + try: + db = get_db() + client = setup_client("anthropic") + + workflow = RepoSummarizerWorkflow( + client=client, + prompts=PROMPTS, + repo_url=repo_url, + ) + + result = workflow.run() + if result.get("success"): + submission = Submission( + task_id=task_id, + round_number=round_number, + status="summarized", + repo_url=repo_url, + pr_url=result["data"]["pr_url"], + ) + db.add(submission) + db.commit() + return jsonify({"success": True, "result": result}) + else: + return jsonify( + {"success": False, "result": result.get("error", "No result")} + ) + except Exception as e: + logger.error(f"Repo summarizer failed: {str(e)}") + raise + + +if __name__ == "__main__": + from flask import Flask + + app = Flask(__name__) + with app.app_context(): + result = handle_task_creation( + task_id="1", + round_number=6, + repo_url="https://github.com/koii-network/builder-test", + ) + print(result) diff --git a/worker/orca-agent/src/server/services/star_service.py b/worker/orca-agent/src/server/services/star_service.py new file mode 100644 index 0000000..07ce1eb --- /dev/null +++ b/worker/orca-agent/src/server/services/star_service.py @@ -0,0 +1,50 @@ +"""Task service module.""" + +import requests +import os +from flask import jsonify +from prometheus_swarm.database import get_db +from prometheus_swarm.clients import setup_client +from src.workflows.repoSummarizer.workflow import RepoSummarizerWorkflow +from prometheus_swarm.utils.logging import logger, log_error +from src.workflows.starRepo.workflow import StarRepoWorkflow +from dotenv import load_dotenv +from src.workflows.repoSummarizer.prompts import PROMPTS + +load_dotenv() + + +def handle_star_task(task_id, round_number, github_urls): + """Handle task creation request.""" + try: + db = get_db() + client = setup_client("anthropic") + for url in github_urls: + star_workflow = StarRepoWorkflow( + client=client, + prompts=PROMPTS, + repo_url=url, + ) + star_result = star_workflow.run() + if not star_result or not star_result.get("success"): + log_error( + Exception(star_result.get("error", "No result")), + "Repository star failed", + ) + return jsonify({"success": True, "result": "Repository starred"}) + except Exception as e: + logger.error(f"Repo summarizer failed: {str(e)}") + raise + + +if __name__ == "__main__": + from flask import Flask + + app = Flask(__name__) + with app.app_context(): + result = handle_star_task( + task_id="1", + round_number=6, + github_urls=["https://github.com/koii-network/builder-test"], + ) + print(result) diff --git a/worker/orca-agent/src/types.py b/worker/orca-agent/src/types.py new file mode 100644 index 0000000..439a7ea --- /dev/null +++ b/worker/orca-agent/src/types.py @@ -0,0 +1,93 @@ +from typing import Dict, Any, Optional, List, TypedDict, Union, Literal, Callable + + +class ToolDefinition(TypedDict): + """Standard internal tool definition format.""" + + name: str + description: str + parameters: Dict[str, str] # JSON Schema object + required: List[str] + final_tool: bool + function: Callable + + +class ToolCall(TypedDict): + """Format for a tool call made by the LLM.""" + + id: str # Unique identifier for this tool call + name: str # name of tool being called + arguments: Dict[str, Any] + + +class ToolOutput(TypedDict): + """Standard output format for all tools. + + All tools must return a response in this format. + The message field contains a human-readable description of what happened, + which will be an error message if success is False. + """ + + success: bool # Whether the tool execution was successful + message: str # Human-readable message about what happened (error message if success is False) + data: Optional[Dict[str, Any]] # Optional structured data from the tool + + +class ToolResponse(TypedDict): + """Format for a tool execution response. + + Wraps a tool's output with its call ID for client handling. + """ + + tool_call_id: str # ID of the tool call this is responding to + output: ToolOutput # The actual output from the tool + + +class PhaseResult(TypedDict): + """Format for a phase result.""" + + success: bool + data: Dict[str, Any] + error: Optional[str] + + +class ToolChoice(TypedDict): + """Configuration for tool usage.""" + + type: Literal["optional", "required", "required_any"] + tool: Optional[str] # Required only when type is "required" + + +class ToolConfig(TypedDict): + """Configuration for tool usage.""" + + tool_definitions: List[ToolDefinition] + tool_choice: ToolChoice + + +class TextContent(TypedDict): + """Format for plain text content.""" + + type: Literal["text"] + text: str + + +class ToolCallContent(TypedDict): + """Format for tool call content.""" + + type: Literal["tool_call"] + tool_call: ToolCall + + +class ToolResponseContent(TypedDict): + """Format for tool response content.""" + + type: Literal["tool_response"] + tool_response: ToolResponse + + +class MessageContent(TypedDict): + """Standard internal message format.""" + + role: Literal["user", "assistant", "system", "tool"] + content: Union[str, List[Union[TextContent, ToolCall, ToolResponseContent]]] diff --git a/worker/orca-agent/src/workflows/repoSummarizer/__main__.py b/worker/orca-agent/src/workflows/repoSummarizer/__main__.py new file mode 100644 index 0000000..da33595 --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizer/__main__.py @@ -0,0 +1,52 @@ +"""Entry point for the todo creator workflow.""" + +import sys +import argparse +from dotenv import load_dotenv +from src.workflows.repoSummarizer.workflow import RepoSummarizerWorkflow +from src.workflows.repoSummarizer.prompts import PROMPTS +from prometheus_swarm.clients import setup_client + +# Load environment variables +load_dotenv() + + +def main(): + """Run the todo creator workflow.""" + parser = argparse.ArgumentParser( + description="Create tasks from a feature specification for a GitHub repository" + ) + parser.add_argument( + "--repo", + type=str, + required=True, + help="GitHub repository URL (e.g., https://github.com/owner/repo)", + ) + + parser.add_argument( + "--model", + type=str, + default="anthropic", + choices=["anthropic", "openai", "xai"], + help="Model provider to use (default: anthropic)", + ) + args = parser.parse_args() + + # Initialize client + client = setup_client(args.model) + + # Run the todo creator workflow + workflow = RepoSummarizerWorkflow( + client=client, + prompts=PROMPTS, + repo_url=args.repo, + ) + + result = workflow.run() + if not result or not result.get("success"): + print("Todo creator workflow failed") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/worker/orca-agent/src/workflows/repoSummarizer/phases.py b/worker/orca-agent/src/workflows/repoSummarizer/phases.py new file mode 100644 index 0000000..a715b0b --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizer/phases.py @@ -0,0 +1,67 @@ +"""Task decomposition workflow phases implementation.""" + +from prometheus_swarm.workflows.base import WorkflowPhase, Workflow + + + + + +class BranchCreationPhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="create_branch", + available_tools=["create_branch"], + conversation_id=conversation_id, + name="Branch Creation", + ) + + +class RepoClassificationPhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="classify_repository", + available_tools=["read_file", "list_files", "classify_repository"], + conversation_id=conversation_id, + name="Repository Classificati on", + ) + + +class ReadmeGenerationPhase(WorkflowPhase): + def __init__( + self, workflow: Workflow, conversation_id: str = None, prompt_name: str = None + ): + super().__init__( + workflow=workflow, + prompt_name=prompt_name, + available_tools=[ + "read_file", + "list_files", + "write_file", + ], + conversation_id=conversation_id, + name="Readme Generation", + ) + + +class ReadmeReviewPhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="review_readme_file", + available_tools=["read_file", "list_files", "review_readme_file"], + conversation_id=conversation_id, + name="Readme Review", + ) + + +class CreatePullRequestPhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="create_pr", + available_tools=["read_file", "list_files", "create_pull_request_legacy"], + conversation_id=conversation_id, + name="Create Pull Request", + ) diff --git a/worker/orca-agent/src/workflows/repoSummarizer/prompts.py b/worker/orca-agent/src/workflows/repoSummarizer/prompts.py new file mode 100644 index 0000000..2364222 --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizer/prompts.py @@ -0,0 +1,593 @@ +"""Prompts for the repository summarization workflow.""" + +PROMPTS = { + "system_prompt": ( + "You are an expert software architect and technical lead specializing in summarizing " + "repositories into comprehensive documentation. You excel at analyzing codebases " + "and creating clear, structured documentation." + ), + "create_branch": ( + "You need to create a feature branch for the README generation.\n" + "Create a new branch with a descriptive name related to creating a README file.\n" + ), + "classify_repository": ( + "Analyze the repository structure and identify the type of repository this is.\n" + "Use the `classify_repository` tool to report your choice.\n" + "You must choose one of the following repository types:\n" + "- Library/SDK: Code meant to be imported and used by other developers\n" + "- Web App: Frontend or full-stack web application\n" + "- API Service: Server-side application providing APIs\n" + "- Mobile App: Native or cross-platform mobile app\n" + "- Tutorial: Educational repository demonstrating techniques\n" + "- Template: Starter code for new projects\n" + "- CLI Tool: Command-line interface application\n" + "- Framework: Foundational structure for building applications\n" + "- Data Science: Machine learning or data analysis project\n" + "- Plugin: Extension or module for a larger system (e.g., CMS, IDE, platform)\n" + "- Chrome Extension: Browser extension targeting the Chrome platform\n" + "- Jupyter Notebook: Interactive code notebooks, often for demos or research\n" + "- Infrastructure: Configuration or automation code (e.g., Docker, Terraform)\n" + "- Smart Contract: Blockchain smart contracts, typically written in Solidity, Rust, etc.\n" + "- DApp: Decentralized application with both smart contract and frontend components\n" + "- Game: Codebase for a game or game engine (2D, 3D, or browser-based)\n" + "- Desktop App: GUI application for desktop environments (e.g., Electron, Qt, Tauri)\n" + "- Dataset: Repository containing structured data for analysis or training\n" + "- Other: If it doesn't fit into any of the above categories\n" + ), + "create_pr": ( + "You are creating a pull request for the documentation you have generated:\n" + "IMPORTANT: Always use relative paths (e.g., 'src/file.py' not '/src/file.py')\n\n" + "Steps to create the pull request:\n" + "1. First examine the available files to understand the implementation\n" + "2. Create a clear and descriptive PR title\n" + "3. Write a comprehensive PR description that includes:\n" + " - Description of all changes made\n" + " - The main features and value of the documentation\n" + ), + "library": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a software library intended" + " for use by developers.\n\n" + "Your README should be formatted in Markdown and include clearly defined section headers.\n\n" + "Please include the following sections:\n" + "1. **Project Overview**\n" + " - A concise description of what the library does\n" + " - Its main purpose and the problems it solves\n" + " - Key features and benefits\n\n" + "2. **Installation**\n" + " - Instructions for installing the library using relevant package managers (e.g., npm, pip, etc.)\n" + " - Mention any prerequisites if applicable\n\n" + "3. **API Reference**\n" + " - Generate a complete list of all publicly exported functions, classes, and constants from the library\n" + " - For each item, include:\n" + " - Its name\n" + " - Description of what it does\n" + " - Function signature with types and descriptions of parameters and return values\n" + " - Example usage\n" + " - Do not omit any significant exports — include everything that would be relevant to a developer using " + "this library\n" + " - Group related items (e.g., utility functions, configuration, components) under subsections if helpful\n" + "4. **Repository Structure**\n" + " - Briefly explain the purpose of key directories and files\n\n" + "5. **Contributing**\n" + " - Include basic instructions for how others can contribute\n" + " - Mention where to find or how to run tests (if available)\n\n" + "6. **License**\n" + " - State the type of license and include a link to the license file\n\n" + "Additional notes:\n" + "- Use bullet points and code blocks to improve readability\n" + "- Keep language friendly but technical and precise\n" + "- If configuration or extension points exist, explain them clearly\n\n" + ), + "web_app": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a web application " + "project.\n\n" + "Format the output using Markdown with clear section headers and proper formatting.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Describe the purpose and core functionality of the application\n" + " - Highlight key features and typical use cases\n\n" + "2. **Getting Started**\n" + " - Provide setup instructions to run the app locally\n" + " - Include steps for installing dependencies and starting the development server\n" + " - Mention any required environment variables and how to configure them (e.g., `.env` file)\n\n" + "3. **Deployment**\n" + " - Describe how to build and deploy the application to production\n" + " - Include relevant deployment commands and target platforms (e.g., Vercel, Netlify, Docker)\n\n" + "4. **Project Structure**\n" + " - Briefly explain the purpose of major folders and files (e.g., `src/`, `public/`, `components/`)\n\n" + "5. **Technologies Used**\n" + " - List the main frameworks, libraries, and tools (e.g., React, Vue, Vite, Tailwind)\n\n" + "6. **Feature Highlights**\n" + " - Describe core user-facing features or flows (e.g., authentication, dashboards, routing)\n\n" + "7. **Configuration**\n" + " - Mention any configurable options, build settings, or plugins used\n\n" + "8. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use bullet points, code blocks, and links where appropriate\n" + "- Make sure commands are copy-pasteable\n" + "- Keep language clear and helpful for developers new to the project" + ), + "api_service": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a backend service that " + "exposes an API (e.g., REST, GraphQL, or similar).\n\n" + "Format the output using Markdown with clear section headers and developer-friendly formatting.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Describe the purpose of the API and its core functionality\n" + " - Highlight key features and typical use cases\n\n" + "2. **Getting Started**\n" + " - Provide setup instructions to run the service locally\n" + " - Include dependency installation and environment variable setup\n" + " - Describe how to start the server in development mode\n\n" + "3. **API Documentation**\n" + " - List the available endpoints or routes\n" + " - For each endpoint, include:\n" + " - Method (GET, POST, etc.)\n" + " - Path and parameters\n" + " - Example request and response\n" + " - Authentication requirements (if any)\n" + " - If an OpenAPI/Swagger spec or GraphQL schema exists, link to it\n\n" + "4. **Authentication**\n" + " - Describe how authentication works (e.g., API keys, OAuth, JWT)\n" + " - Include example headers or auth flow steps if needed\n\n" + "5. **Project Structure**\n" + " - Explain key folders and files, such as `routes/`, `controllers/`, `models/`\n\n" + "6. **Technologies Used**\n" + " - List major frameworks, libraries, or tools (e.g., Express, FastAPI, Prisma)\n\n" + "7. **Deployment**\n" + " - Describe how to deploy the service (e.g., Docker, CI/CD, cloud platforms)\n" + " - Include environment config or scaling considerations if relevant\n\n" + "8. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use bullet points, code blocks, and sample payloads for clarity\n" + "- Focus on making the API easy to understand and consume\n" + "- Keep the tone clear and helpful for developers using the API" + ), + "mobile_app": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a mobile application " + "project.\n\n" + "Format the output using Markdown with clear section headers and mobile developer–friendly formatting.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Describe the purpose and core functionality of the app\n" + " - List key features and intended user experience\n\n" + "2. **Supported Platforms**\n" + " - Indicate whether the app runs on Android, iOS, or both\n" + " - Mention any platform-specific dependencies or limitations\n\n" + "3. **Getting Started**\n" + " - Provide setup instructions for running the app locally\n" + " - Include steps for installing dependencies and required SDKs (e.g., Android Studio, Xcode)\n" + " - Describe how to configure environment variables or API keys\n\n" + "4. **Running the App**\n" + " - Show commands to run the app on a simulator/emulator or real device\n" + " - Include platform-specific commands if needed (e.g., `npx react-native run-ios`, `flutter run`)\n\n" + "5. **Project Structure**\n" + " - Briefly explain the layout of important folders and files (e.g., `src/`, `ios/`, `android/`, `lib/`)\n\n" + "6. **Technologies Used**\n" + " - List the frameworks, SDKs, and libraries used (e.g., React Native, Flutter, Firebase)\n\n" + "7. **Key Screens and Features**\n" + " - Highlight core screens or flows within the app (e.g., login, profile, dashboard)\n" + " - Optionally include screenshots or descriptions of user interactions\n\n" + "8. **Build and Deployment**\n" + " - Provide steps for creating production builds\n" + " - Mention any tools or services used for distribution (e.g., TestFlight, Play Store, Expo)\n\n" + "9. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use bullet points, code blocks, and platform-specific sections where needed\n" + "- Make sure setup steps work for both Android and iOS where applicable\n" + "- Keep the tone clear and helpful for mobile developers" + ), + "tutorial": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is designed as an educational " + "tutorial or learning resource.\n\n" + "Format the output using Markdown with clear section headers and a logical, beginner-friendly structure.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Summarize the goal of the tutorial and what users will learn\n" + " - List key topics or technologies covered\n" + " - Mention any prerequisites (e.g., knowledge of a language, tools to install)\n\n" + "2. **Getting Started**\n" + " - Provide step-by-step setup instructions\n" + " - Include installation of dependencies, toolchain setup, and environment config\n" + " - Ensure instructions work on major operating systems\n\n" + "3. **Tutorial Structure**\n" + " - Break down the tutorial into sections, stages, or lessons\n" + " - Briefly describe what each section teaches or builds\n" + " - Link to key files or folders associated with each part\n\n" + "4. **Learning Outcomes**\n" + " - Clearly list the skills or concepts users will have mastered by the end\n\n" + "5. **Code Examples and Exercises**\n" + " - Mention inline code snippets, checkpoints, or interactive examples\n" + " - If exercises are included, describe how users should complete or test them\n\n" + "6. **Project Structure**\n" + " - Describe the layout of the repository and which files correspond to different tutorial stages\n\n" + "7. **Next Steps / Further Reading**\n" + " - Suggest where users can go after completing the tutorial\n" + " - Include links to additional docs, libraries, or related tutorials\n\n" + "8. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use beginner-friendly language without dumbing things down\n" + "- Include code blocks, links, and visual structure to aid readability\n" + "- Help users stay oriented by reminding them what they've done and what's next" + ), + "template": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which serves as a project starter or " + "boilerplate template.\n\n" + "Format the output using Markdown with clear section headers and developer-friendly formatting.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Describe the purpose of this template and the type of projects it's meant for\n" + " - List key features, tools, or configurations included by default\n\n" + "2. **Getting Started**\n" + " - Provide instructions for cloning or copying the template\n" + " - Include setup steps: installing dependencies, environment config, and running locally\n\n" + "3. **Customization Guide**\n" + " - Explain which parts of the codebase are intended to be modified by users\n" + " - Offer guidance on how to rename, rebrand, or restructure parts of the template\n\n" + "4. **Project Structure**\n" + " - Describe the layout of important directories and files\n" + " - Highlight which files are meant for customization vs. boilerplate\n\n" + "5. **Technologies Used**\n" + " - List the frameworks, libraries, and tools integrated into the template (e.g., ESLint, Prettier, " + "Tailwind, Express)\n\n" + "6. **Use Cases**\n" + " - Provide example scenarios where this template is useful (e.g., 'Use this for building a REST API with " + "authentication')\n" + " - Link to live demos or projects built from this template if available\n\n" + "7. **Contributing**\n" + " - If the template is open to contributions, provide basic instructions for submitting improvements\n\n" + "8. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Focus on helping users get started quickly and confidently\n" + "- Use code blocks and examples to show how things work\n" + "- Encourage best practices and provide defaults users can trust or extend" + ), + "cli_tool": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a command-line " + "interface (CLI) tool.\n\n" + "Format the output using Markdown with clear section headers and include clear command-line examples.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Explain what the CLI tool does and why it's useful\n" + " - Mention common use cases or problems it solves\n\n" + "2. **Installation**\n" + " - Provide steps to install the tool (e.g., npm, pip, Homebrew, binary download)\n" + " - Mention any required dependencies or environment setup\n\n" + "3. **Usage**\n" + " - Show how to use the tool from the command line\n" + " - Include at least 2–3 example commands with explanations of the output\n" + " - Demonstrate the most common and useful flags or options\n" + " - If the tool supports subcommands, show examples of each\n\n" + "4. **Command Reference**\n" + " - List all available commands, flags, and options in a table or list format\n" + " - Explain each option clearly, including defaults and accepted values\n\n" + "5. **Configuration (if applicable)**\n" + " - Describe any optional or required configuration files (e.g., `.clirc`, `config.json`)\n" + " - Show example configurations and where to place them\n\n" + "6. **Project Structure**\n" + " - Briefly describe key files or folders related to the CLI's source code\n\n" + "7. **Contributing**\n" + " - Outline how to contribute, test changes, or add new commands\n\n" + "8. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use code blocks for command examples and outputs\n" + "- Keep tone practical and clear, suitable for developers or power users\n" + "- Focus on usability and real-world examples of the tool in action" + ), + "framework": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a software framework " + "designed to be extended or used as a foundation for building applications.\n\n" + "Format the output using Markdown with clear section headers and structured, developer-friendly formatting.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Describe what the framework does and the type of projects it's built for\n" + " - Highlight key concepts and design philosophy (e.g., convention over configuration, modularity)\n\n" + "2. **Getting Started**\n" + " - Include steps for installing and initializing a new project using the framework\n" + " - Provide a minimal working example with code blocks\n\n" + "3. **Core Concepts**\n" + " - Explain the main components or building blocks (e.g., modules, services, lifecycle, routing, etc.)\n" + " - Include diagrams or conceptual overviews if helpful\n\n" + "4. **Extension Points**\n" + " - Describe how developers can extend the framework (e.g., plugins, middleware, custom components)\n" + " - Include examples of common extension use cases\n\n" + "5. **Project Structure**\n" + " - Explain the directory layout of a typical project using the framework\n" + " - Highlight where user code should live and where internal framework logic resides\n\n" + "6. **Technologies Used**\n" + " - List core dependencies, supported environments, or language-level features leveraged\n\n" + "7. **Best Practices**\n" + " - Offer guidance for structuring large projects, writing maintainable code, or following framework " + "conventions\n\n" + "8. **Contributing**\n" + " - Outline how contributors can report issues, add features, or build plugins for the framework\n\n" + "9. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use clear examples and code snippets to explain key abstractions\n" + "- Keep the tone empowering and oriented toward other developers building on top of the framework\n" + "- Emphasize extensibility and conceptual clarity" + ), + "data_science": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a data science or " + "machine learning project.\n\n" + "Format the output using Markdown with clear section headers and helpful formatting for technical readers.\n\n" + "Include the following sections:\n" + "1. **Project Overview**\n" + " - Explain the goal of the project (e.g., prediction, classification, analysis)\n" + " - Summarize key findings or outcomes (if applicable)\n\n" + "2. **Dataset**\n" + " - Describe the dataset used (source, size, structure)\n" + " - Include schema information or link to external data sources\n" + " - Mention whether the data is included in the repo or needs to be downloaded\n\n" + "3. **Installation and Setup**\n" + " - List dependencies and setup instructions (e.g., `requirements.txt`, `environment.yml`)\n" + " - Mention any additional setup (e.g., downloading data, creating folders)\n\n" + "4. **Project Structure**\n" + " - Explain the layout of scripts, notebooks, data folders, and model outputs\n" + " - Highlight the main entry points for running the pipeline\n\n" + "5. **Model Architecture and Training**\n" + " - Briefly describe the model(s) used and why they were chosen\n" + " - Include training scripts and command-line instructions\n" + " - Mention metrics used for evaluation\n\n" + "6. **Evaluation and Results**\n" + " - Summarize how the model was evaluated and key performance metrics\n" + " - Optionally include plots, confusion matrices, or sample outputs\n\n" + "7. **Inference / How to Use the Model**\n" + " - Explain how to run inference or apply the model to new data\n" + " - Include input/output formats and example commands or code\n\n" + "8. **Technologies Used**\n" + " - List key tools, libraries, and frameworks (e.g., scikit-learn, TensorFlow, pandas)\n\n" + "9. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Use code blocks and examples where appropriate\n" + "- Ensure reproducibility by including all necessary setup instructions\n" + "- Keep the tone professional and geared toward data scientists or ML engineers" + ), + "plugin": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a plugin or extension " + "designed to integrate with a larger platform (such as a CMS, IDE, or framework).\n\n" + "Format the output using Markdown with clear section headers.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe what this plugin does and the host system it's built for\n" + " - List key features and benefits\n\n" + "2. **Installation**\n" + " - Provide installation instructions specific to the host platform\n" + " - Mention compatible versions and any prerequisites\n\n" + "3. **Usage**\n" + " - Show how to enable and configure the plugin\n" + " - Include code snippets or configuration steps\n\n" + "4. **Integration Points**\n" + " - Describe hooks, lifecycle methods, or extension APIs the plugin interacts with\n\n" + "5. **Project Structure**\n" + " - Briefly explain key files and folders\n\n" + "6. **Technologies Used**\n" + " - List frameworks, languages, or tooling\n\n" + "7. **License**\n" + " - State the license type and link to the license file" + ), + "chrome_extension": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a Chrome extension " + "project.\n\n" + "Format the output using Markdown with clear section headers.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe the purpose and features of the extension\n\n" + "2. **Installation**\n" + " - Include instructions for loading the extension in Chrome (via the Extensions page or Chrome Web Store)\n" + " - Mention required permissions and how to review the manifest\n\n" + "3. **Usage**\n" + " - Explain how users interact with the extension (e.g., popup UI, context menu, background scripts)\n" + " - Include example scenarios or screenshots if applicable\n\n" + "4. **Project Structure**\n" + " - Briefly describe key files like `manifest.json`, `background.js`, and popup components\n\n" + "5. **Technologies Used**\n" + " - List libraries or frameworks (e.g., vanilla JS, React, Tailwind)\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "jupyter_notebook": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which consists of one or more " + "Jupyter notebooks.\n\n" + "Format the output using Markdown with clear section headers.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe the purpose of the notebooks and what they demonstrate or analyze\n\n" + "2. **Getting Started**\n" + " - Provide instructions for setting up the environment (e.g., installing Jupyter, dependencies, " + "virtualenv)\n" + " - Mention how to launch the notebooks (e.g., `jupyter notebook` or `jupyter lab`)\n\n" + "3. **Notebook Summary**\n" + " - List and briefly describe each notebook in the repo\n" + " - Mention whether they build on each other or are standalone\n\n" + "4. **Dataset (if applicable)**\n" + " - Describe any datasets used and where they come from\n\n" + "5. **Technologies Used**\n" + " - List libraries (e.g., pandas, matplotlib, scikit-learn)\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "infrastructure": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which contains " + "infrastructure-as-code or deployment configuration (e.g., Docker, Terraform, Ansible).\n\n" + "Format the output using Markdown with clear section headers.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Explain what infrastructure is being managed and its intended use\n\n" + "2. **Setup**\n" + " - Describe any prerequisites (e.g., installing Docker, Terraform CLI, cloud access credentials)\n" + " - Include instructions for initializing and applying the configuration\n\n" + "3. **Configuration Files**\n" + " - Explain the structure and purpose of major files (e.g., `main.tf`, `docker-compose.yml`, " + "`playbooks/`)\n\n" + "4. **Deployment Workflow**\n" + " - Describe how deployments are triggered and verified\n" + " - Mention any CI/CD pipelines, remote state management, or secrets handling\n\n" + "5. **Environments**\n" + " - Clarify how to deploy to multiple environments (dev, staging, prod)\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "smart_contract": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which contains smart contracts " + "written for a blockchain platform (e.g., Ethereum, Solana).\n\n" + "Format the output using Markdown with clear section headers.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Explain the purpose and functionality of the smart contracts\n" + " - Mention the target blockchain platform\n\n" + "2. **Installation and Setup**\n" + " - List dependencies and setup instructions (e.g., hardhat, anchor, solana-cli)\n" + " - Include local devnet instructions if applicable\n\n" + "3. **Contracts**\n" + " - Describe the main contract(s) and what each one does\n" + " - Include deployment steps and how to interact with them\n\n" + "4. **Testing**\n" + " - Explain how to run tests and what framework is used\n\n" + "5. **Project Structure**\n" + " - Describe layout of contracts, migrations, and test files\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "dapp": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a decentralized application " + "(dApp) that includes both smart contract(s) and a web-based frontend.\n\n" + "Format the output using Markdown with clear section headers and examples for both on-chain and off-chain " + "components.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe what the dApp does and the blockchain ecosystem it runs on\n" + " - Mention the smart contract platform (e.g., Ethereum, Solana, NEAR) and wallet compatibility\n\n" + "2. **Architecture**\n" + " - Provide a high-level diagram or explanation of how the frontend interacts with smart contracts\n" + " - Mention key technologies used on both sides (e.g., React, Ethers.js, Anchor, Web3.js)\n\n" + "3. **Getting Started**\n" + " - Provide setup instructions for both frontend and backend\n" + " - Include how to install dependencies, configure environment variables, and run locally\n\n" + "4. **Smart Contracts**\n" + " - Describe the deployed contracts and how to interact with them\n" + " - Include deployment instructions and test commands\n\n" + "5. **Frontend**\n" + " - Describe key UI components and user flows (e.g., connect wallet, mint token, submit vote)\n" + " - Mention any integrations with IPFS, oracles, or off-chain data\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "game": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a game or game engine " + "project.\n\n" + "Format the output using Markdown with clear section headers and provide clear instructions for playing and " + "modifying the game.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe the game concept, genre, and platform (e.g., browser, desktop, mobile)\n" + " - Mention gameplay goals or mechanics\n\n" + "2. **Installation and Setup**\n" + " - Provide instructions for installing dependencies and running the game\n" + " - Include setup for game engines or SDKs (e.g., Unity, Godot, Phaser, Unreal)\n\n" + "3. **Controls and Gameplay**\n" + " - Explain player controls and core mechanics\n" + " - Optionally include screenshots, video, or demo links\n\n" + "4. **Project Structure**\n" + " - Describe key files and folders (e.g., assets, levels, scripts)\n\n" + "5. **Technologies Used**\n" + " - List engines, frameworks, or libraries used to build the game\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "desktop_app": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which is a desktop application " + "project built with technologies like Electron, Tauri, Qt, or native frameworks.\n\n" + "Format the output using Markdown with clear section headers and platform-aware instructions.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe what the desktop app does and who it's for\n" + " - Mention platforms supported (e.g., Windows, macOS, Linux)\n\n" + "2. **Installation and Setup**\n" + " - Provide platform-specific install/build instructions\n" + " - Include steps for running the app in development and building a production release\n\n" + "3. **Usage**\n" + " - Describe the app's main features and user workflows\n" + " - Include screenshots if applicable\n\n" + "4. **Project Structure**\n" + " - Describe key files and folders (e.g., main process, renderer process, assets)\n\n" + "5. **Technologies Used**\n" + " - List major libraries, frameworks, and build tools\n\n" + "6. **License**\n" + " - State the license type and link to the license file" + ), + "dataset": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository, which contains a dataset for " + "analysis, training, or research purposes.\n\n" + "Format the output using Markdown with clear section headers and data-focused structure.\n\n" + "Include the following sections:\n" + "1. **Overview**\n" + " - Describe what the dataset contains and its intended purpose\n" + " - Mention the source and whether it was collected, generated, or aggregated\n\n" + "2. **Dataset Details**\n" + " - Describe the structure and format (e.g., CSV, JSON, images, text)\n" + " - Include column definitions, schema, or data dictionaries\n" + " - Mention the number of records, size, and any notable characteristics\n\n" + "3. **Usage Instructions**\n" + " - Provide example code snippets for loading and using the dataset (e.g., pandas, SQL, etc.)\n" + " - Mention any preprocessing steps if needed\n\n" + "4. **Licensing and Terms of Use**\n" + " - State the license and any restrictions on usage or distribution\n" + " - Include citation or attribution instructions if required\n\n" + "5. **Related Work / Source Links**\n" + " - Link to original data sources, research papers, or related projects (if applicable)" + ), + "other": ( + "Please scan the repository and generate or update a complete and professional readme_prometheus.md file for this repository.\n\n" + "{previous_review_comments_section}\n\n" + "Analyze the contents of the repository to infer its intent, and format the README using Markdown with " + "clear section headers.\n\n" + "Include the following general sections, customizing them as needed based on the repository type:\n" + "1. **Project Overview**\n" + " - Describe the purpose of the project and its main functionality\n" + " - Summarize what the project does and who it's for\n\n" + "2. **Getting Started**\n" + " - Include setup or usage instructions based on the repo's structure\n" + " - Mention installation steps, dependencies, and commands to run or use the project\n\n" + "3. **Features / Capabilities**\n" + " - List the core features or components of the project\n" + " - Include relevant examples, demos, or configurations if applicable\n\n" + "4. **Project Structure**\n" + " - Describe the layout of files and folders, especially any key scripts, configs, or assets\n\n" + "5. **Technologies Used**\n" + " - List any major frameworks, libraries, or languages identified in the project\n\n" + "6. **Usage Examples** (if applicable)\n" + " - Include example commands or steps showing how to use the project\n\n" + "7. **License**\n" + " - State the license type and link to the license file\n\n" + "Additional Notes:\n" + "- Focus on making the README useful and descriptive, even if the project type is ambiguous\n" + "- Use best judgment to tailor the content to the actual functionality and audience of the project\n" + "- Avoid placeholder text and strive to extract real, useful information from the codebase" + ), + "review_readme_file": ( + "Review the readme_prometheus.md file in the repository and evaluate its quality and relevance to the repository.\n\n" + "Please analyze:\n" + "1. Is the readme_prometheus.md file related to this specific repository? (Does it describe the actual code and purpose of this repo?)\n" + "2. Does it correctly explain the repository's purpose, features, and functionality?\n" + "3. Is it comprehensive enough to help users understand and use the repository?\n" + "4. Does it follow best practices for README documentation?\n\n" + "Use the validate_implementation tool to submit your findings.\n" + "STOP after submitting the review report." + ), + "previous_review_comments": ( + "Here are the comments from the previous review:\n" + ), +} + + + + diff --git a/worker/orca-agent/src/workflows/repoSummarizer/workflow.py b/worker/orca-agent/src/workflows/repoSummarizer/workflow.py new file mode 100644 index 0000000..0597016 --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizer/workflow.py @@ -0,0 +1,277 @@ +"""Task decomposition workflow implementation.""" + +import os +from github import Github +from prometheus_swarm.workflows.base import Workflow +from prometheus_swarm.utils.logging import log_section, log_key_value, log_error +from src.workflows.repoSummarizer import phases +from prometheus_swarm.workflows.utils import ( + check_required_env_vars, + cleanup_repository, + validate_github_auth, + setup_repository +) +from src.workflows.repoSummarizer.prompts import PROMPTS + + +class Task: + def __init__(self, title: str, description: str, acceptance_criteria: list[str]): + self.title = title + self.description = description + self.acceptance_criteria = acceptance_criteria + + def to_dict(self) -> dict: + """Convert task to dictionary format.""" + return { + "title": self.title, + "description": self.description, + "acceptance_criteria": self.acceptance_criteria, + } + + @classmethod + def from_dict(cls, data: dict) -> "Task": + """Create task from dictionary.""" + return cls( + title=data["title"], + description=data["description"], + acceptance_criteria=data["acceptance_criteria"], + ) + + +class RepoSummarizerWorkflow(Workflow): + def __init__( + self, + client, + prompts, + repo_url, + ): + # Extract owner and repo name from URL + # URL format: https://github.com/owner/repo + parts = repo_url.strip("/").split("/") + repo_owner = parts[-2] + repo_name = parts[-1] + + super().__init__( + client=client, + prompts=prompts, + repo_url=repo_url, + repo_owner=repo_owner, + repo_name=repo_name, + ) + + def setup(self): + """Set up repository and workspace.""" + check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"]) + validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME")) + + # Get the default branch from GitHub + try: + gh = Github(os.getenv("GITHUB_TOKEN")) + self.context["repo_full_name"] = ( + f"{self.context['repo_owner']}/{self.context['repo_name']}" + ) + + repo = gh.get_repo( + f"{self.context['repo_owner']}/{self.context['repo_name']}" + ) + self.context["base"] = repo.default_branch + log_key_value("Default branch", self.context["base"]) + except Exception as e: + log_error(e, "Failed to get default branch, using 'main'") + self.context["base"] = "main" + + # Set up repository directory + setup_result = setup_repository(self.context["repo_url"], github_token=os.getenv("GITHUB_TOKEN"), github_username=os.getenv("GITHUB_USERNAME")) + if not setup_result["success"]: + raise Exception(f"Failed to set up repository: {setup_result['message']}") + self.context["github_token"] = os.getenv("GITHUB_TOKEN") + self.context["repo_path"] = setup_result["data"]["clone_path"] + self.original_dir = setup_result["data"]["original_dir"] + self.context["fork_url"] = setup_result["data"]["fork_url"] + self.context["fork_owner"] = setup_result["data"]["fork_owner"] + self.context["fork_name"] = setup_result["data"]["fork_name"] + + # Enter repo directory + os.chdir(self.context["repo_path"]) + + # Configure Git user info + # setup_git_user_config(self.context["repo_path"]) + + # Get current files for context + + def cleanup(self): + """Cleanup workspace.""" + # Make sure we're not in the repo directory before cleaning up + if os.getcwd() == self.context.get("repo_path", ""): + os.chdir(self.original_dir) + + # Clean up the repository directory + cleanup_repository(self.original_dir, self.context.get("repo_path", "")) + # Clean up the MongoDB + + def run(self): + self.setup() + + # Create a feature branch + log_section("CREATING FEATURE BRANCH") + branch_phase = phases.BranchCreationPhase(workflow=self) + branch_result = branch_phase.execute() + + if not branch_result or not branch_result.get("success"): + log_error(Exception("Branch creation failed"), "Branch creation failed") + return { + "success": False, + "message": "Branch creation failed", + "data": None, + } + + # Store branch name in context + self.context["head"] = branch_result["data"]["branch_name"] + log_key_value("Branch created", self.context["head"]) + + # Classify repository + repo_classification_result = self.classify_repository() + if not repo_classification_result or not repo_classification_result.get( + "success" + ): + log_error( + Exception("Repository classification failed"), + "Repository classification failed", + ) + return { + "success": False, + "message": "Repository classification failed", + "data": None, + } + + # Get prompt name for README generation + prompt_name = repo_classification_result["data"].get("prompt_name") + if not prompt_name: + log_error( + Exception("No prompt name returned from repository classification"), + "Repository classification failed to provide prompt name", + ) + return { + "success": False, + "message": "Repository classification failed to provide prompt name", + "data": None, + } + + # Generate README file + for i in range(3): + if i > 0: + prompt_name = "other" + readme_result = self.generate_readme_file(prompt_name) + if not readme_result or not readme_result.get("success"): + log_error(Exception("README generation failed"), "README generation failed") + return { + "success": False, + "message": "README generation failed", + "data": None, + } + if readme_result.get("success"): + review_result = self.review_readme_file(readme_result) + if not review_result or not review_result.get("success"): + log_error(Exception("README review failed"), "README review failed") + return { + "success": False, + "message": "README review failed", + "data": None, + } + log_key_value("README review result", review_result.get("data")) + if review_result.get("success") and review_result.get("data").get("recommendation") == "APPROVE": + result = self.create_pull_request() + return result + else: + self.context["previous_review_comments_section"] = PROMPTS["previous_review_comments"] + review_result.get("data").get("comment") + + + + + return { + "success": False, + "message": "README Review Exceed Max Attempts", + "data": None, + } + + def classify_repository(self): + try: + log_section("CLASSIFYING REPOSITORY TYPE") + repo_classification_phase = phases.RepoClassificationPhase(workflow=self) + return repo_classification_phase.execute() + except Exception as e: + log_error(e, "Repository classification workflow failed") + return { + "success": False, + "message": f"Repository classification workflow failed: {str(e)}", + "data": None, + } + def review_readme_file(self, readme_result): + """Execute the issue generation workflow.""" + try: + log_section("REVIEWING README FILE") + review_readme_file_phase = phases.ReadmeReviewPhase(workflow=self) + return review_readme_file_phase.execute() + except Exception as e: + log_error(e, "Readme file review workflow failed") + return { + "success": False, + "message": f"Readme file review workflow failed: {str(e)}", + "data": None, + } + + def generate_readme_file(self, prompt_name): + """Execute the issue generation workflow.""" + try: + + # ==================== Generate README file ==================== + log_section("GENERATING README FILE") + generate_readme_file_phase = phases.ReadmeGenerationPhase( + workflow=self, prompt_name=prompt_name + ) + readme_result = generate_readme_file_phase.execute() + + # Check README Generation Result + if not readme_result or not readme_result.get("success"): + log_error( + Exception(readme_result.get("error", "No result")), + "Readme file generation failed", + ) + return None + + return readme_result + + except Exception as e: + log_error(e, "Readme file generation workflow failed") + return { + "success": False, + "message": f"Readme file generation workflow failed: {str(e)}", + "data": None, + } + + def create_pull_request(self): + """Create a pull request for the README file.""" + try: + log_section("CREATING PULL REQUEST") + + # Add required PR title and description parameters to context + self.context["title"] = f"Prometheus: Add README for {self.context['repo_name']}" + self.context["description"] = ( + f"This PR adds a README file for the {self.context['repo_name']} repository." + ) + + log_key_value( + "Creating PR", + f"from {self.context['head']} to {self.context['base']}", + ) + + print("CONTEXT", self.context) + create_pull_request_phase = phases.CreatePullRequestPhase(workflow=self) + return create_pull_request_phase.execute() + except Exception as e: + log_error(e, "Pull request creation workflow failed") + return { + "success": False, + "message": f"Pull request creation workflow failed: {str(e)}", + "data": None, + } diff --git a/worker/orca-agent/src/workflows/repoSummarizerAudit/__main__.py b/worker/orca-agent/src/workflows/repoSummarizerAudit/__main__.py new file mode 100644 index 0000000..4d517bf --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizerAudit/__main__.py @@ -0,0 +1,52 @@ +"""Entry point for the todo creator workflow.""" + +import sys +import argparse +from dotenv import load_dotenv +from src.workflows.repoSummarizerAudit.workflow import repoSummarizerAuditWorkflow +from src.workflows.repoSummarizerAudit.prompts import PROMPTS +from prometheus_swarm.clients import setup_client + +# Load environment variables +load_dotenv() + + +def main(): + """Run the todo creator workflow.""" + parser = argparse.ArgumentParser( + description="Create tasks from a feature specification for a GitHub repository" + ) + parser.add_argument( + "--pr-url", + type=str, + required=True, + help="GitHub pull request URL (e.g., https://github.com/owner/repo/pull/1)", + ) + + parser.add_argument( + "--model", + type=str, + default="anthropic", + choices=["anthropic", "openai", "xai"], + help="Model provider to use (default: anthropic)", + ) + args = parser.parse_args() + + # Initialize client + client = setup_client(args.model) + + # Run the todo creator workflow + workflow = repoSummarizerAuditWorkflow( + client=client, + prompts=PROMPTS, + pr_url=args.pr_url, + ) + + result = workflow.run() + if not result or not result.get("success"): + print("Todo creator workflow failed") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/worker/orca-agent/src/workflows/repoSummarizerAudit/phases.py b/worker/orca-agent/src/workflows/repoSummarizerAudit/phases.py new file mode 100644 index 0000000..d1f4fa3 --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizerAudit/phases.py @@ -0,0 +1,15 @@ +"""Task decomposition workflow phases implementation.""" + +from prometheus_swarm.workflows.base import WorkflowPhase, Workflow + + +class CheckReadmeFilePhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="check_readme_file", + available_tools=["read_file", "list_files", "review_pull_request_legacy"], + conversation_id=conversation_id, + name="Check Readme File", + ) + diff --git a/worker/orca-agent/src/workflows/repoSummarizerAudit/prompts.py b/worker/orca-agent/src/workflows/repoSummarizerAudit/prompts.py new file mode 100644 index 0000000..f1c2746 --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizerAudit/prompts.py @@ -0,0 +1,29 @@ +"""Prompts for the repository summarization workflow.""" + +PROMPTS = { + "system_prompt": ( + "You are an expert software architect and technical lead specializing in summarizing " + "repositories into comprehensive documentation. You excel at analyzing codebases " + "and creating clear, structured documentation." + ), + + "check_readme_file": ( + "A pull request has been checked out for you. The repository is {repo_owner}/{repo_name} and " + "the PR number is {pr_number}. The following files are available:\n" + "{current_files}\n\n" + "The criteria for the README file are:\n" + "1. Project Overview\n" + " - Purpose and main functionality\n" + " - Key features\n" + "2. Repository Structure\n" + " - Detailed breakdown of directories and their purposes\n" + " - Key files and their roles\n" + "3. Technical Details\n" + " - Technologies used\n" + " - Architecture overview\n" + "4. File Contents\n" + " - Specific description of each significant file\n\n" + "Please review the README file and give feedback.\n" + ), + +} diff --git a/worker/orca-agent/src/workflows/repoSummarizerAudit/workflow.py b/worker/orca-agent/src/workflows/repoSummarizerAudit/workflow.py new file mode 100644 index 0000000..db78499 --- /dev/null +++ b/worker/orca-agent/src/workflows/repoSummarizerAudit/workflow.py @@ -0,0 +1,163 @@ +"""Task decomposition workflow implementation.""" + +import os +from github import Github +from prometheus_swarm.workflows.base import Workflow +from prometheus_swarm.utils.logging import log_section, log_key_value, log_error +from src.workflows.repoSummarizerAudit import phases +from prometheus_swarm.workflows.utils import ( + check_required_env_vars, + validate_github_auth, + setup_repository, + cleanup_repository, + get_current_files, +) +from git import Repo + + +class Task: + def __init__(self, title: str, description: str, acceptance_criteria: list[str]): + self.title = title + self.description = description + self.acceptance_criteria = acceptance_criteria + + def to_dict(self) -> dict: + """Convert task to dictionary format.""" + return { + "title": self.title, + "description": self.description, + "acceptance_criteria": self.acceptance_criteria, + } + + @classmethod + def from_dict(cls, data: dict) -> "Task": + """Create task from dictionary.""" + return cls( + title=data["title"], + description=data["description"], + acceptance_criteria=data["acceptance_criteria"], + ) + + +class repoSummarizerAuditWorkflow(Workflow): + def __init__( + self, + client, + prompts, + pr_url, + ): + # Extract owner and repo name from URL + # URL format: https://github.com/owner/repo + parts = pr_url.strip("/").split("/") + repo_owner = parts[-4] + repo_name = parts[-3] + pr_number = int(parts[-1]) # Convert to integer + super().__init__( + client=client, + prompts=prompts, + repo_owner=repo_owner, + repo_name=repo_name, + pr_number=pr_number, + ) + self.context["pr_number"] = pr_number + self.context["pr_url"] = pr_url + self.context["repo_owner"] = repo_owner + self.context["repo_name"] = repo_name + self.context["repo_full_name"] = f"{repo_owner}/{repo_name}" + + def setup(self): + """Set up repository and workspace.""" + # Check required environment variables and validate GitHub auth + check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"]) + validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME")) + self.context["repo_url"] = f"https://github.com/{self.context['repo_owner']}/{self.context['repo_name']}" + # Set up repository directory + setup_result = setup_repository(self.context["repo_url"], github_token=os.getenv("GITHUB_TOKEN"), github_username=os.getenv("GITHUB_USERNAME")) + if not setup_result["success"]: + raise Exception(f"Failed to set up repository: {setup_result['message']}") + + self.context["repo_path"] = setup_result["data"]["clone_path"] + self.original_dir = setup_result["data"]["original_dir"] + self.context["fork_url"] = setup_result["data"]["fork_url"] + self.context["fork_owner"] = setup_result["data"]["fork_owner"] + self.context["fork_name"] = setup_result["data"]["fork_name"] + self.context["github_token"] = os.getenv("GITHUB_TOKEN") + # Enter repo directory + os.chdir(self.context["repo_path"]) + gh = Github(self.context["github_token"]) + repo = gh.get_repo( + f"{self.context['repo_owner']}/{self.context['repo_name']}" + ) + pr = repo.get_pull(self.context["pr_number"]) + self.context["pr"] = pr + # Add remote for PR's repository and fetch the branch + os.system( + f"git remote add pr_source https://github.com/{pr.head.repo.full_name}" + ) + os.system(f"git fetch pr_source {pr.head.ref}") + os.system("git checkout FETCH_HEAD") + + # Get current files for context + self.context["current_files"] = get_current_files() + + def cleanup(self): + """Cleanup workspace.""" + # Make sure we're not in the repo directory before cleaning up + if os.getcwd() == self.context.get("repo_path", ""): + os.chdir(self.original_dir) + + # Clean up the repository directory + cleanup_repository(self.original_dir, self.context.get("repo_path", "")) + # Clean up the MongoDB + + def run(self): + check_readme_file_result = self.check_readme_file() + + return check_readme_file_result + + def check_readme_file(self): + """Execute the issue generation workflow.""" + try: + self.setup() + # ==================== Generate issues ==================== + check_readme_file_phase = phases.CheckReadmeFilePhase(workflow=self) + check_readme_file_result = check_readme_file_phase.execute() + # Check Issue Generation Result + if not check_readme_file_result or not check_readme_file_result.get( + "success" + ): + log_error( + Exception(check_readme_file_result.get("error", "No result")), + "Readme file check failed", + ) + return { + "success": False, + "message": "Readme file check failed", + "data": { + "recommendation": False, + }, + } + log_section("Readme file check completed") + print(check_readme_file_result) + recommendation = check_readme_file_result["data"]["recommendation"] + log_key_value( + "Readme file check completed", f"Recommendation: {recommendation}" + ) + # Star the repository + return { + "success": True, + "message": "Readme file check completed", + "data": { + "recommendation": recommendation == "APPROVE", + }, + } + except Exception as e: + log_error(e, "Readme file check workflow failed") + print(e) + return { + "success": False, + "message": f"Readme file check workflow failed: {str(e)}", + "data": { + "recommendation": False, + }, + } diff --git a/worker/orca-agent/src/workflows/starRepo/__main__.py b/worker/orca-agent/src/workflows/starRepo/__main__.py new file mode 100644 index 0000000..c4b91a9 --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepo/__main__.py @@ -0,0 +1,57 @@ +"""Entry point for the todo creator workflow.""" + +import sys +import os +import argparse +from dotenv import load_dotenv +from src.workflows.starRepo.workflow import StarRepoWorkflow +from src.workflows.starRepo.prompts import PROMPTS +from prometheus_swarm.clients import setup_client + +# Load environment variables +load_dotenv() + + +def main(): + """Run the todo creator workflow.""" + parser = argparse.ArgumentParser( + description="Create tasks from a feature specification for a GitHub repository" + ) + parser.add_argument( + "--repo", + type=str, + required=True, + help="GitHub repository URL (e.g., https://github.com/owner/repo)", + ) + + parser.add_argument( + "--model", + type=str, + default="anthropic", + choices=["anthropic", "openai", "xai"], + help="Model provider to use (default: anthropic)", + ) + args = parser.parse_args() + + # Initialize client + client = setup_client(args.model) + + # Run the todo creator workflow + workflow = StarRepoWorkflow( + client=client, + prompts=PROMPTS, + repo_url=args.repo, + ) + + + result = workflow.run() + if not result or not result.get("success"): + print("Todo creator workflow failed") + sys.exit(1) + + + + + +if __name__ == "__main__": + main() diff --git a/worker/orca-agent/src/workflows/starRepo/phases.py b/worker/orca-agent/src/workflows/starRepo/phases.py new file mode 100644 index 0000000..b9e8015 --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepo/phases.py @@ -0,0 +1,15 @@ +"""Task decomposition workflow phases implementation.""" + +from prometheus_swarm.workflows.base import WorkflowPhase, Workflow + + +class ReadmeGenerationPhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="generate_readme_file", + available_tools=["read_file", "write_file", "list_files", "commit_and_push"], + conversation_id=conversation_id, + name="Readme Generation", + ) + diff --git a/worker/orca-agent/src/workflows/starRepo/prompts.py b/worker/orca-agent/src/workflows/starRepo/prompts.py new file mode 100644 index 0000000..aa99c1d --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepo/prompts.py @@ -0,0 +1,29 @@ +"""Prompts for the repository summarization workflow.""" + +PROMPTS = { + "system_prompt": ( + "You are an expert software architect and technical lead specializing in summarizing " + "repositories into comprehensive documentation. You excel at analyzing codebases " + "and creating clear, structured documentation." + ), + + "generate_readme_file": ( + "Generate a comprehensive README file for the following repository:\n" + "Repository: {repo_url}\n\n" + "Please include:\n" + "1. Project Overview\n" + " - Purpose and main functionality\n" + " - Key features\n" + "2. Repository Structure\n" + " - Detailed breakdown of directories and their purposes\n" + " - Key files and their roles\n" + "3. Technical Details\n" + " - Technologies used\n" + " - Architecture overview\n" + "4. File Contents\n" + " - Specific description of each significant file\n\n" + "Format the output in markdown, ensuring clear section headers and proper formatting." + "Please commit and push the changes to the repository after generating the README file." + ), + +} diff --git a/worker/orca-agent/src/workflows/starRepo/workflow.py b/worker/orca-agent/src/workflows/starRepo/workflow.py new file mode 100644 index 0000000..ce7b74b --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepo/workflow.py @@ -0,0 +1,141 @@ +"""Task decomposition workflow implementation.""" + +import os +from github import Github +from prometheus_swarm.workflows.base import Workflow +from prometheus_swarm.tools.github_operations.implementations import star_repository +from prometheus_swarm.utils.logging import log_section, log_key_value, log_error +from src.workflows.repoSummarizer import phases +from prometheus_swarm.workflows.utils import ( + check_required_env_vars, + validate_github_auth, +) + + +class Task: + def __init__(self, title: str, description: str, acceptance_criteria: list[str]): + self.title = title + self.description = description + self.acceptance_criteria = acceptance_criteria + + def to_dict(self) -> dict: + """Convert task to dictionary format.""" + return { + "title": self.title, + "description": self.description, + "acceptance_criteria": self.acceptance_criteria, + } + + @classmethod + def from_dict(cls, data: dict) -> "Task": + """Create task from dictionary.""" + return cls( + title=data["title"], + description=data["description"], + acceptance_criteria=data["acceptance_criteria"], + ) + + +class StarRepoWorkflow(Workflow): + def __init__( + self, + client, + prompts, + repo_url, + ): + # Extract owner and repo name from URL + # URL format: https://github.com/owner/repo + parts = repo_url.strip("/").split("/") + repo_owner = parts[-2] + repo_name = parts[-1] + + super().__init__( + client=client, + prompts=prompts, + repo_url=repo_url, + repo_owner=repo_owner, + repo_name=repo_name, + ) + self.context["repo_owner"] = repo_owner + self.context["repo_name"] = repo_name + self.context["github_token"] = os.getenv("GITHUB_TOKEN") + + def setup(self): + """Set up repository and workspace.""" + check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"]) + validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME")) + + # # Get the default branch from GitHub + # try: + # gh = Github(os.getenv("GITHUB_TOKEN")) + # repo = gh.get_repo( + # f"{self.context['repo_owner']}/{self.context['repo_name']}" + # ) + # self.context["base_branch"] = repo.default_branch + # log_key_value("Default branch", self.context["base_branch"]) + # except Exception as e: + # log_error(e, "Failed to get default branch, using 'main'") + # self.context["base_branch"] = "main" + + # Set up repository directory + # repo_path, original_dir = setup_repo_directory() + # self.context["repo_path"] = repo_path + # self.original_dir = original_dir + + # # Fork and clone repository + # log_section("FORKING AND CLONING REPOSITORY") + # fork_result = fork_repository( + # f"{self.context['repo_owner']}/{self.context['repo_name']}", + # self.context["repo_path"], + # ) + # if not fork_result["success"]: + # error = fork_result.get("error", "Unknown error") + # log_error(Exception(error), "Fork failed") + # raise Exception(error) + + # # Enter repo directory + # os.chdir(self.context["repo_path"]) + + # # Configure Git user info + # setup_git_user_config(self.context["repo_path"]) + + # Get current files for context + + def cleanup(self): + """Cleanup workspace.""" + # cleanup_repository(self.original_dir, self.context.get("repo_path", "")) + # Make sure we're not in the repo directory before cleaning up + # if os.getcwd() == self.context.get("repo_path", ""): + # os.chdir(self.original_dir) + + # # Clean up the repository directory + # cleanup_repo_directory(self.original_dir, self.context.get("repo_path", "")) + # Clean up the MongoDB + + def run(self): + star_repo_result = self.start_star_repo() + return star_repo_result + + def start_star_repo(self): + """Execute the issue generation workflow.""" + try: + self.setup() + # ==================== Generate issues ==================== + star_repo_result = star_repository( + self.context["repo_owner"], self.context["repo_name"], self.context["github_token"] + ) + if not star_repo_result or not star_repo_result.get("success"): + log_error( + Exception(star_repo_result.get("error", "No result")), + "Repository star failed", + ) + return None + return star_repo_result + except Exception as e: + log_error(e, "Readme file generation workflow failed") + print(e) + return { + "success": False, + "message": f"Readme file generation workflow failed: {str(e)}", + "data": None, + } diff --git a/worker/orca-agent/src/workflows/starRepoAudit/__main__.py b/worker/orca-agent/src/workflows/starRepoAudit/__main__.py new file mode 100644 index 0000000..5a2df2e --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepoAudit/__main__.py @@ -0,0 +1,58 @@ +"""Entry point for the todo creator workflow.""" + +import sys +import os +import argparse +from dotenv import load_dotenv +from src.workflows.starRepoAudit.workflow import StarRepoAuditWorkflow +from src.workflows.starRepoAudit.prompts import PROMPTS +from prometheus_swarm.clients import setup_client + +# Load environment variables +load_dotenv() + + +def main(): + """Run the todo creator workflow.""" + parser = argparse.ArgumentParser( + description="Create tasks from a feature specification for a GitHub repository" + ) + parser.add_argument( + "--repo", + type=str, + required=True, + help="GitHub repository URL (e.g., https://github.com/owner/repo)", + ) + + parser.add_argument( + "--model", + type=str, + default="anthropic", + choices=["anthropic", "openai", "xai"], + help="Model provider to use (default: anthropic)", + ) + args = parser.parse_args() + + # Initialize client + client = setup_client(args.model) + + # Run the todo creator workflow + workflow = StarRepoAuditWorkflow( + client=client, + prompts=PROMPTS, + repo_url=args.repo, + github_username="HermanL02", + ) + + + result = workflow.run() + if not result or not result.get("success"): + print("Todo creator workflow failed") + sys.exit(1) + + + + + +if __name__ == "__main__": + main() diff --git a/worker/orca-agent/src/workflows/starRepoAudit/phases.py b/worker/orca-agent/src/workflows/starRepoAudit/phases.py new file mode 100644 index 0000000..5f32861 --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepoAudit/phases.py @@ -0,0 +1,15 @@ +"""Task decomposition workflow phases implementation.""" + +from src.workflows.base import WorkflowPhase, Workflow + + +class ReadmeGenerationPhase(WorkflowPhase): + def __init__(self, workflow: Workflow, conversation_id: str = None): + super().__init__( + workflow=workflow, + prompt_name="generate_readme_file", + available_tools=["read_file", "write_file", "list_files", "commit_and_push"], + conversation_id=conversation_id, + name="Readme Generation", + ) + diff --git a/worker/orca-agent/src/workflows/starRepoAudit/prompts.py b/worker/orca-agent/src/workflows/starRepoAudit/prompts.py new file mode 100644 index 0000000..aa99c1d --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepoAudit/prompts.py @@ -0,0 +1,29 @@ +"""Prompts for the repository summarization workflow.""" + +PROMPTS = { + "system_prompt": ( + "You are an expert software architect and technical lead specializing in summarizing " + "repositories into comprehensive documentation. You excel at analyzing codebases " + "and creating clear, structured documentation." + ), + + "generate_readme_file": ( + "Generate a comprehensive README file for the following repository:\n" + "Repository: {repo_url}\n\n" + "Please include:\n" + "1. Project Overview\n" + " - Purpose and main functionality\n" + " - Key features\n" + "2. Repository Structure\n" + " - Detailed breakdown of directories and their purposes\n" + " - Key files and their roles\n" + "3. Technical Details\n" + " - Technologies used\n" + " - Architecture overview\n" + "4. File Contents\n" + " - Specific description of each significant file\n\n" + "Format the output in markdown, ensuring clear section headers and proper formatting." + "Please commit and push the changes to the repository after generating the README file." + ), + +} diff --git a/worker/orca-agent/src/workflows/starRepoAudit/workflow.py b/worker/orca-agent/src/workflows/starRepoAudit/workflow.py new file mode 100644 index 0000000..59cd7b9 --- /dev/null +++ b/worker/orca-agent/src/workflows/starRepoAudit/workflow.py @@ -0,0 +1,151 @@ +"""Task decomposition workflow implementation.""" + +import os +from github import Github +from prometheus_swarm.workflows.base import Workflow +from prometheus_swarm.tools.github_operations.implementations import ( + get_user_starred_repos, +) +from prometheus_swarm.utils.logging import log_section, log_key_value, log_error +from src.workflows.repoSummarizer import phases +from prometheus_swarm.workflows.utils import ( + check_required_env_vars, + validate_github_auth, +) + + +class Task: + def __init__(self, title: str, description: str, acceptance_criteria: list[str]): + self.title = title + self.description = description + self.acceptance_criteria = acceptance_criteria + + def to_dict(self) -> dict: + """Convert task to dictionary format.""" + return { + "title": self.title, + "description": self.description, + "acceptance_criteria": self.acceptance_criteria, + } + + @classmethod + def from_dict(cls, data: dict) -> "Task": + """Create task from dictionary.""" + return cls( + title=data["title"], + description=data["description"], + acceptance_criteria=data["acceptance_criteria"], + ) + + +class StarRepoAuditWorkflow(Workflow): + def __init__( + self, + client, + prompts, + repo_url, + github_username, + ): + # Extract owner and repo name from URL + # URL format: https://github.com/owner/repo + parts = repo_url.strip("/").split("/") + repo_owner = parts[-2] + repo_name = parts[-1] + + super().__init__( + client=client, + prompts=prompts, + repo_url=repo_url, + repo_owner=repo_owner, + repo_name=repo_name, + github_username=github_username, + ) + self.context["repo_owner"] = repo_owner + self.context["repo_name"] = repo_name + self.context["github_username"] = github_username + + def setup(self): + """Set up repository and workspace.""" + check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"]) + validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME")) + + # # Get the default branch from GitHub + # try: + # gh = Github(os.getenv("GITHUB_TOKEN")) + # repo = gh.get_repo( + # f"{self.context['repo_owner']}/{self.context['repo_name']}" + # ) + # self.context["base_branch"] = repo.default_branch + # log_key_value("Default branch", self.context["base_branch"]) + # except Exception as e: + # log_error(e, "Failed to get default branch, using 'main'") + # self.context["base_branch"] = "main" + + # # Set up repository directory + # repo_path, original_dir = setup_repo_directory() + # self.context["repo_path"] = repo_path + # self.original_dir = original_dir + + # # Fork and clone repository + # log_section("FORKING AND CLONING REPOSITORY") + # fork_result = fork_repository( + # f"{self.context['repo_owner']}/{self.context['repo_name']}", + # self.context["repo_path"], + # ) + # if not fork_result["success"]: + # error = fork_result.get("error", "Unknown error") + # log_error(Exception(error), "Fork failed") + # raise Exception(error) + + # # Enter repo directory + # os.chdir(self.context["repo_path"]) + + # # Configure Git user info + # setup_git_user_config(self.context["repo_path"]) + + # # Get current files for context + + def cleanup(self): + """Cleanup workspace.""" + # # Make sure we're not in the repo directory before cleaning up + # if os.getcwd() == self.context.get("repo_path", ""): + # os.chdir(self.original_dir) + + # # Clean up the repository directory + # cleanup_repo_directory(self.original_dir, self.context.get("repo_path", "")) + # Clean up the MongoDB + + def run(self): + star_repo_result = self.check_star_repo() + if not star_repo_result: + log_error( + Exception("Repository is not starred"), "Repository is not starred" + ) + return False + return star_repo_result + + def check_star_repo(self): + """Check if the repository is starred.""" + try: + print(self.context["github_username"]) + + starred_repos = get_user_starred_repos(self.context["github_username"]) + print(starred_repos) + if not starred_repos or not starred_repos.get("success"): + log_error( + Exception(starred_repos.get("error", "No result")), + "Failed to get starred repositories", + ) + return False + # check if the repository is in the starred_repos + if f"{self.context['repo_owner']}/{self.context['repo_name']}" in [ + repo["full_name"] for repo in starred_repos["data"]["starred_repos"] + ]: + print("Repository is starred") + return {"success": True, "result": "Repository is starred"} + else: + print("Repository is not starred") + return {"success": False, "result": "Repository is not starred"} + except Exception as e: + log_error(e, "Failed to check if repository is starred") + return False diff --git a/worker/package.json b/worker/package.json new file mode 100644 index 0000000..4620063 --- /dev/null +++ b/worker/package.json @@ -0,0 +1,76 @@ +{ + "name": "orca-task", + "version": "2.2.0", + "description": "", + "main": "src/index.js", + "type": "module", + "scripts": { + "build": "tsc", + "test": "npm run build && node build/tests/simulateTask.js", + "jest-test": "jest --detectOpenHandles", + "start": "node index.js", + "prod-debug": "nodemon --ignore 'dist/*' tests/prod-debug.js", + "webpack": "rm -rf dist && rm -rf node_modules && yarn && webpack", + "webpack:test": "webpack --config tests/test.webpack.config.js", + "webpack:prod": "webpack --mode production", + "webpack:dev": "webpack", + "rename-dist": "mv dist/main.js dist/bafybeianaylvcqh42l7pitsboznlpni3b2gqh2n6jbldmm6oty36ldisra.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@_koii/orca-node": "^0.1.18", + "@_koii/storage-task-sdk": "^1.2.7", + "@_koii/task-manager": "^1.0.13", + "@_koii/web3.js": "^0.1.11", + "@octokit/rest": "^21.1.1", + "axios": "^1.7.2", + "cross-spawn": "^7.0.3", + "dotenv": "^16.5.0", + "dotenv-webpack": "^8.1.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "nodemon": "^3.1.0", + "seedrandom": "^3.0.5", + "tail": "^2.2.6" + }, + "peerDependencies": { + "@_koii/namespace-wrapper": "^1.0.23" + }, + "devDependencies": { + "@_koii/namespace-wrapper": "^1.0.23", + "@babel/preset-env": "^7.26.0", + "@babel/preset-typescript": "^7.26.0", + "@types/cross-spawn": "^6.0.6", + "@types/eslint": "^9.6.1", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/js-yaml": "^4.0.9", + "@types/lodash": "^4.17.9", + "@types/node": "^22.14.1", + "@types/seedrandom": "^3.0.8", + "@types/tail": "^2.2.3", + "@typescript-eslint/eslint-plugin": "^8.7.0", + "@typescript-eslint/parser": "^8.7.0", + "axios": "^1.7.2", + "babel-jest": "^29.7.0", + "chalk": "^5.3.0", + "cross-spawn": "^7.0.3", + "dotenv-webpack": "^8.1.0", + "eslint": "^8.57.0", + "globals": "^15.9.0", + "jest": "^29.7.0", + "joi": "^17.9.2", + "prettier": "^3.3.3", + "tail": "^2.2.6", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "tsx": "^4.19.3", + "typescript": "^5.6.2", + "webpack": "^5.28.0", + "webpack-cli": "^4.5.0" + }, + "engines": { + "node": ">=18.17.0" + } +} diff --git a/worker/src/index.ts b/worker/src/index.ts new file mode 100644 index 0000000..2314fbb --- /dev/null +++ b/worker/src/index.ts @@ -0,0 +1,20 @@ +import { initializeTaskManager } from "@_koii/task-manager"; +import { setup } from "./task/0-setup"; +import { task } from "./task/1-task"; +import { submission } from "./task/2-submission"; +import { audit } from "./task/3-audit"; +import { distribution } from "./task/4-distribution"; +import { routes } from "./task/5-routes"; + +import { initializeOrcaClient } from "@_koii/task-manager/extensions"; +import { getConfig } from "./orcaSettings"; + +initializeTaskManager({ + setup, + task, + submission, + audit, + distribution, + routes, +}); +initializeOrcaClient(getConfig); diff --git a/worker/src/orcaSettings.ts b/worker/src/orcaSettings.ts new file mode 100644 index 0000000..2bc8f99 --- /dev/null +++ b/worker/src/orcaSettings.ts @@ -0,0 +1,46 @@ +import { TASK_ID, namespaceWrapper } from "@_koii/namespace-wrapper"; +import "dotenv/config"; + +const imageUrl = "docker.io/hermanyiqunliang/summarizer-agent:0.2"; + +async function createPodSpec(): Promise { + const basePath = await namespaceWrapper.getBasePath(); + + const podSpec = `apiVersion: v1 +kind: Pod +metadata: + name: 247-builder-test +spec: + containers: + - name: user-${TASK_ID} + image: ${imageUrl} + env: + - name: GITHUB_TOKEN + value: "${process.env.GITHUB_TOKEN}" + - name: GITHUB_USERNAME + value: "${process.env.GITHUB_USERNAME}" + - name: ANTHROPIC_API_KEY + value: "${process.env.ANTHROPIC_API_KEY}" + volumeMounts: + - name: builder-data + mountPath: /data + volumes: + - name: builder-data + hostPath: + path: ${basePath}/orca/data + type: DirectoryOrCreate +`; + return podSpec; +} + +export async function getConfig(): Promise<{ + imageURL: string; + customPodSpec: string; + rootCA: string | null; +}> { + return { + imageURL: imageUrl, + customPodSpec: await createPodSpec(), + rootCA: null, + }; +} diff --git a/worker/src/task/0-setup.ts b/worker/src/task/0-setup.ts new file mode 100644 index 0000000..29522cb --- /dev/null +++ b/worker/src/task/0-setup.ts @@ -0,0 +1,4 @@ +export async function setup(): Promise { + // define any steps that must be executed before the task starts + console.log("CUSTOM SETUP"); +} diff --git a/worker/src/task/1-task.ts b/worker/src/task/1-task.ts new file mode 100644 index 0000000..6a37aef --- /dev/null +++ b/worker/src/task/1-task.ts @@ -0,0 +1,215 @@ +import { getOrcaClient } from "@_koii/task-manager/extensions"; +import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper"; +import "dotenv/config"; +import { getRandomNodes } from "../utils/leader"; +import { getExistingIssues } from "../utils/existingIssues"; +import { status, middleServerUrl } from "../utils/constant"; +import dotenv from "dotenv"; +import { checkAnthropicAPIKey, isValidAnthropicApiKey } from "../utils/anthropicCheck"; +import { checkGitHub } from "../utils/githubCheck"; +import { LogLevel } from "@_koii/namespace-wrapper/dist/types"; +import { actionMessage } from "../utils/constant"; +import { errorMessage } from "../utils/constant"; +dotenv.config(); + + +export async function task(roundNumber: number): Promise { + /** + * Run your task and store the proofs to be submitted for auditing + * It is expected you will store the proofs in your container + * The submission of the proofs is done in the submission function + */ + // FORCE TO PAUSE 30 SECONDS +// No submission on Round 0 so no need to trigger fetch audit result before round 3 +// Changed from 3 to 4 to have more time + if (roundNumber >= 4) { + const triggerFetchAuditResult = await fetch(`${middleServerUrl}/api/builder/summarizer/trigger-fetch-audit-result`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ taskId: TASK_ID, round: roundNumber - 4 }) + }); + console.log(`[TASK] Trigger fetch audit result for round ${roundNumber - 3}. Result is ${triggerFetchAuditResult.status}.`); + } + console.log(`[TASK] EXECUTE TASK FOR ROUND ${roundNumber}`); + try { + const orcaClient = await getOrcaClient(); + // check if the env variable is valid + if (!process.env.ANTHROPIC_API_KEY) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.ANTHROPIC_API_KEY_INVALID, actionMessage.ANTHROPIC_API_KEY_INVALID); + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ANTHROPIC_API_KEY_INVALID); + return; + } + if (!isValidAnthropicApiKey(process.env.ANTHROPIC_API_KEY!)) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.ANTHROPIC_API_KEY_INVALID, actionMessage.ANTHROPIC_API_KEY_INVALID); + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ANTHROPIC_API_KEY_INVALID); + return; + } + const isAnthropicAPIKeyValid = await checkAnthropicAPIKey(process.env.ANTHROPIC_API_KEY!); + if (!isAnthropicAPIKeyValid) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.ANTHROPIC_API_KEY_NO_CREDIT, actionMessage.ANTHROPIC_API_KEY_NO_CREDIT); + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ANTHROPIC_API_KEY_NO_CREDIT); + return; + } + if (!process.env.GITHUB_USERNAME || !process.env.GITHUB_TOKEN) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.GITHUB_CHECK_FAILED, actionMessage.GITHUB_CHECK_FAILED); + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.GITHUB_CHECK_FAILED); + return; + } + const isGitHubValid = await checkGitHub(process.env.GITHUB_USERNAME!, process.env.GITHUB_TOKEN!); + if (!isGitHubValid) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.GITHUB_CHECK_FAILED, actionMessage.GITHUB_CHECK_FAILED); + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.GITHUB_CHECK_FAILED); + return; + } + if (!orcaClient) { + await namespaceWrapper.logMessage(LogLevel.Error, errorMessage.NO_ORCA_CLIENT, actionMessage.NO_ORCA_CLIENT); + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ORCA_CLIENT); + return; + } + + const stakingKeypair = await namespaceWrapper.getSubmitterAccount(); + if (!stakingKeypair) { + throw new Error("No staking keypair found"); + } + const stakingKey = stakingKeypair.publicKey.toBase58(); + const pubKey = await namespaceWrapper.getMainAccountPubkey(); + if (!pubKey) { + throw new Error("No public key found"); + } + /****************** All issues need to be starred ******************/ + + const existingIssues = await getExistingIssues(); + const githubUrls = existingIssues.map((issue) => issue.githubUrl); + try { + await orcaClient.podCall(`star/${roundNumber}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ taskId: TASK_ID, round_number: String(roundNumber), github_urls: githubUrls }), + }); + } catch (error) { + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.STAR_ISSUE_FAILED); + console.error("Error starring issues:", error); + } + /****************** All these issues need to be generate a markdown file ******************/ + + const signature = await namespaceWrapper.payloadSigning( + { + taskId: TASK_ID, + roundNumber: roundNumber, + action: "fetch", + githubUsername: stakingKey, + stakingKey: stakingKey + }, + stakingKeypair.secretKey, + ); + + // const initializedDocumentSummarizeIssues = await getInitializedDocumentSummarizeIssues(existingIssues); + + console.log(`[TASK] Making Request to Middle Server with taskId: ${TASK_ID} and round: ${roundNumber}`); + const requiredWorkResponse = await fetch(`${middleServerUrl}/api/builder/summarizer/fetch-summarizer-todo`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ signature: signature, stakingKey: stakingKey }), + }); + // check if the response is 200 + if (requiredWorkResponse.status !== 200) { + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ISSUES_PENDING_TO_BE_SUMMARIZED); + return; + } + const requiredWorkResponseData = await requiredWorkResponse.json(); + console.log("[TASK] requiredWorkResponseData: ", requiredWorkResponseData); + + const jsonBody = { + taskId: TASK_ID, + round_number: String(roundNumber), + repo_url: `https://github.com/${requiredWorkResponseData.data.repo_owner}/${requiredWorkResponseData.data.repo_name}`, + }; + console.log("[TASK] jsonBody: ", jsonBody); + try { + const repoSummaryResponse = await orcaClient.podCall(`repo_summary/${roundNumber}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(jsonBody), + }); + console.log("[TASK] repoSummaryResponse: ", repoSummaryResponse); + console.log("[TASK] repoSummaryResponse.data.result.data ", repoSummaryResponse.data.result.data); + const payload = { + taskId: TASK_ID, + action: "add", + roundNumber: roundNumber, + prUrl: repoSummaryResponse.data.result.data.pr_url, + stakingKey: stakingKey + } + console.log("[TASK] Signing payload: ", payload); + if (repoSummaryResponse.status === 200) { + try{ + const signature = await namespaceWrapper.payloadSigning( + payload, + stakingKeypair.secretKey, + ); + console.log("[TASK] signature: ", signature); + const addPrToSummarizerTodoResponse = await fetch(`${middleServerUrl}/api/builder/summarizer/add-pr-to-summarizer-todo`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ signature: signature, stakingKey: stakingKey }), + }); + console.log("[TASK] addPrToSummarizerTodoResponse: ", addPrToSummarizerTodoResponse); + }catch(error){ + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO); + console.error("[TASK] Error adding PR to summarizer todo:", error); + } + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUCCESSFULLY_SUMMARIZED); + } else { + // post this summary response to slack` to notify the team + // THE HOOK IS ALREADY DISABLED + // try{ + // const slackResponse = await fetch('https://hooks.slack.com/services/', { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify({ + // text: `[TASK] Error summarizing issue:\nStatus: ${repoSummaryResponse.status}\nData: ${JSON.stringify(repoSummaryResponse.data, null, 2)}` + // }), + // }); + // console.log("[TASK] slackResponse: ", slackResponse); + // }catch(error){ + // console.error("[TASK] Error posting to slack:", error); + // } + + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_FAILED_TO_BE_SUMMARIZED); + } + } catch (error) { + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_FAILED_TO_BE_SUMMARIZED); + + // try{ + // const slackResponse = await fetch('https://hooks.slack.com/services', { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify({ + // text: `[TASK] Error summarizing issue:\n ${JSON.stringify(error)}` + // }), + // }); + // console.log("[TASK] slackResponse: ", slackResponse); + // }catch(error){ + // console.error("[TASK] Error posting to slack:", error); + // } + console.error("[TASK] EXECUTE TASK ERROR:", error); + } + } catch (error) { + await namespaceWrapper.storeSet(`result-${roundNumber}`, status.UNKNOWN_ERROR); + console.error("[TASK] EXECUTE TASK ERROR:", error); + } +} diff --git a/worker/src/task/2-submission.ts b/worker/src/task/2-submission.ts new file mode 100644 index 0000000..e7686d3 --- /dev/null +++ b/worker/src/task/2-submission.ts @@ -0,0 +1,102 @@ +import { storeFile } from "../utils/ipfs"; +import { getOrcaClient } from "@_koii/task-manager/extensions"; +import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper"; +import { status } from "../utils/constant"; +export async function submission(roundNumber: number) : Promise { + /** + * Retrieve the task proofs from your container and submit for auditing + * Must return a string of max 512 bytes to be submitted on chain + * The default implementation handles uploading the proofs to IPFS + * and returning the CID + */ + console.log(`[SUBMISSION] Starting submission process for round ${roundNumber}`); + + try { + console.log("[SUBMISSION] Initializing Orca client..."); + const orcaClient = await getOrcaClient(); + if (!orcaClient) { + console.error("[SUBMISSION] Failed to initialize Orca client"); + return; + } + console.log("[SUBMISSION] Orca client initialized successfully"); + + console.log(`[SUBMISSION] Fetching task result for round ${roundNumber}...`); + const taskResult = await namespaceWrapper.storeGet(`result-${roundNumber}`); + if (!taskResult) { + console.log("[SUBMISSION] No task result found for this round"); + return status.NO_DATA_FOR_THIS_ROUND; + } + console.log(`[SUBMISSION] Task result status: ${taskResult}`); + + if (taskResult !== status.ISSUE_SUCCESSFULLY_SUMMARIZED) { + console.log(`[SUBMISSION] Task not successfully summarized. Status: ${taskResult}`); + return taskResult; + } + + console.log(`[SUBMISSION] Fetching submission data for round ${roundNumber}...`); + const result = await orcaClient.podCall(`submission/${roundNumber}`); + let submission; + + console.log("[SUBMISSION] Submission result:", result.data); + + if (result.data === "No submission") { + console.log("[SUBMISSION] No existing submission found, creating new submission object"); + submission = { + githubUsername: process.env.GITHUB_USERNAME, + prUrl: "none", + roundNumber, + }; + } else { + submission = result.data; + } + + console.log("[SUBMISSION] Validating submission data..."); + if (submission.roundNumber !== roundNumber) { + console.error(`[SUBMISSION] Round number mismatch. Expected: ${roundNumber}, Got: ${submission.roundNumber}`); + throw new Error("Submission is not for the current round"); + } + + if (!submission.prUrl) { + console.error("[SUBMISSION] Missing PR URL in submission"); + throw new Error("Submission is missing PR URL"); + } + + console.log("[SUBMISSION] Submission data validated successfully:", submission); + + console.log("[SUBMISSION] Getting submitter account..."); + const stakingKeypair = await namespaceWrapper.getSubmitterAccount(); + + if (!stakingKeypair) { + console.error("[SUBMISSION] No staking keypair found"); + throw new Error("No staking keypair found"); + } + console.log("[SUBMISSION] Submitter account retrieved successfully"); + + const stakingKey = stakingKeypair.publicKey.toBase58(); + const pubKey = await namespaceWrapper.getMainAccountPubkey(); + console.log("[SUBMISSION] Staking key:", stakingKey); + console.log("[SUBMISSION] Public key:", pubKey); + + console.log("[SUBMISSION] Signing submission payload..."); + const signature = await namespaceWrapper.payloadSigning( + { + taskId: TASK_ID, + roundNumber, + stakingKey, + pubKey, + action: "audit", + ...submission, + }, + stakingKeypair.secretKey, + ); + console.log("[SUBMISSION] Payload signed successfully"); + + console.log("[SUBMISSION] Storing submission on IPFS..."); + const cid = await storeFile({ signature }, "submission.json"); + console.log("[SUBMISSION] Submission stored successfully. CID:", cid); + return cid || void 0; + } catch (error) { + console.error("[SUBMISSION] Error during submission process:", error); + throw error; + } +} diff --git a/worker/src/task/3-audit.ts b/worker/src/task/3-audit.ts new file mode 100644 index 0000000..7e3c5c1 --- /dev/null +++ b/worker/src/task/3-audit.ts @@ -0,0 +1,84 @@ +import { getOrcaClient } from "@_koii/task-manager/extensions"; +import { middleServerUrl, status } from "../utils/constant"; +import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode"; +// import { status } from '../utils/constant' +export async function audit(cid: string, roundNumber: number, submitterKey: string): Promise { + /** + * Audit a submission + * This function should return true if the submission is correct, false otherwise + * The default implementation retrieves the proofs from IPFS + * and sends them to your container for auditing + */ + + try { + const orcaClient = await getOrcaClient(); + if (!orcaClient) { + // await namespaceWrapper.storeSet(`result-${roundNumber}`, status.NO_ORCA_CLIENT); + return; + } + // Check if the cid is one of the status + if (Object.values(status).includes(cid)) { + // This returns a dummy true + return true; + } + const decodeResult = await submissionJSONSignatureDecode({submission_value: cid, submitterPublicKey: submitterKey, roundNumber: roundNumber}); + if (!decodeResult) { + console.log("[AUDIT] DECODE RESULT FAILED.") + return false; + } + console.log(`[AUDIT] ✅ Signature decoded successfully`); + + console.log(`[AUDIT] Checking summarizer status for submitter ${submitterKey}`); + const checkSummarizerResponse = await fetch(`${middleServerUrl}/api/builder/summarizer/check-summarizer`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + stakingKey: submitterKey, + roundNumber, + githubUsername: decodeResult.githubUsername, + prUrl: decodeResult.prUrl + }), + }); + const checkSummarizerJSON = await checkSummarizerResponse.json(); + console.log(`[AUDIT] Summarizer check response:`, checkSummarizerJSON); + + if (!checkSummarizerJSON.success) { + console.log(`[AUDIT] ❌ Audit failed for ${submitterKey}`); + return false; + } + console.log(`[AUDIT] ✅ Summarizer check passed`); + + console.log(`[AUDIT] Sending audit request for submitter: ${submitterKey}`); + console.log(`[AUDIT] Submission data being sent to audit:`, decodeResult); + + const result = await orcaClient.podCall(`audit/${roundNumber}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + submission: decodeResult, + }), + }); + + console.log(`[AUDIT] Raw audit result:`, result); + console.log(`[AUDIT] Audit result data type:`, typeof result.data); + console.log(`[AUDIT] Audit result data value:`, result.data); + + if (result.data === true) { + console.log(`[AUDIT] ✅ Audit passed for ${submitterKey}`); + return true; + } else { + console.log(`[AUDIT] ❌ Audit failed for ${submitterKey}`); + console.log(`[AUDIT] Failed audit result data:`, result.data); + return false; + } + } catch (error) { + console.error("[AUDIT] Error auditing submission:", error); + + // When Error---NO RETURN; + // return true; + } +} diff --git a/worker/src/task/4-distribution.ts b/worker/src/task/4-distribution.ts new file mode 100644 index 0000000..2186d13 --- /dev/null +++ b/worker/src/task/4-distribution.ts @@ -0,0 +1,64 @@ +import { Submitter, DistributionList } from "@_koii/task-manager"; +import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper"; +import { customReward, status } from "../utils/constant"; +import { Submission } from "@_koii/namespace-wrapper/dist/types"; +import { middleServerUrl } from "../utils/constant"; +import { getOrcaClient } from "@_koii/task-manager/extensions"; +import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode"; +import { getRandomNodes } from "../utils/leader"; +const getSubmissionList = async (roundNumber: number): Promise> => { + const submissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber); + return submissionInfo?.submissions[roundNumber] || {}; +} +export const getEmptyDistributionList = async ( + submitters: Submitter[], +): Promise => { + const distributionList: DistributionList = {}; + for (const submitter of submitters) { + distributionList[submitter.publicKey] = 0; + } + return distributionList; +} +export const distribution = async ( + submitters: Submitter[], + bounty: number, + roundNumber: number, +): Promise => { + try { + const distributionList: DistributionList = {}; + + for (const submitter of submitters) { + console.log(`\n[DISTRIBUTION] Processing submitter: ${submitter.publicKey}`); + + console.log(`[DISTRIBUTION] Getting submission list for round ${roundNumber}`); + const submitterSubmissions = await getSubmissionList(roundNumber); + console.log(`[DISTRIBUTION] Total submissions found: ${Object.keys(submitterSubmissions).length}`); + + const submitterSubmission = submitterSubmissions[submitter.publicKey]; + if (!submitterSubmission || submitterSubmission.submission_value === "") { + console.log(`[DISTRIBUTION] ❌ No valid submission found for submitter ${submitter.publicKey}`); + distributionList[submitter.publicKey] = 0; + continue; + } + if (Object.values(status).includes(submitterSubmission.submission_value)) { + distributionList[submitter.publicKey] = 0; + continue; + }else{ + // TODO: Check if I should include = 0 here + if (submitter.votes >= 0) { + distributionList[submitter.publicKey] = customReward; + }else{ + distributionList[submitter.publicKey] = 0; + } + } + } + + console.log(`[DISTRIBUTION] ✅ Distribution completed successfully`); + console.log(`[DISTRIBUTION] Final distribution list:`, distributionList); + return distributionList; + } catch (error: any) { + console.error(`[DISTRIBUTION] ❌ ERROR IN DISTRIBUTION:`, error); + console.error(`[DISTRIBUTION] Error stack:`, error.stack); + return {}; + } +}; diff --git a/worker/src/task/5-routes.ts b/worker/src/task/5-routes.ts new file mode 100644 index 0000000..71d006e --- /dev/null +++ b/worker/src/task/5-routes.ts @@ -0,0 +1,57 @@ +import { namespaceWrapper, app } from "@_koii/task-manager/namespace-wrapper"; +import { getLeaderNode, getRandomNodes } from "../utils/leader"; +import { task } from "./1-task"; +import { submission } from "./2-submission"; +import { audit } from "./3-audit"; +import { distribution } from "./4-distribution"; +import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode"; +import { Submission } from "@_koii/namespace-wrapper/dist/types"; +import { taskRunner } from "@_koii/task-manager" + +/** + * + * Define all your custom routes here + * + */ + +//Example route +export async function routes() { + app.get("/value", async (_req, res) => { + const value = await namespaceWrapper.storeGet("value"); + console.log("value", value); + res.status(200).json({ value: value }); + }); + + app.get("/leader/:roundNumber/:submitterPublicKey", async (req, res) => { + const roundNumber = req.params.roundNumber; + const submitterPublicKey = req.params.submitterPublicKey; + const {isLeader, leaderNode} = await getLeaderNode({roundNumber: Number(roundNumber), submitterPublicKey: submitterPublicKey}); + res.status(200).json({ isLeader: isLeader, leaderNode: leaderNode }); + }); + + app.get("/task/:roundNumber", async (req, res) => { + console.log("task endpoint called with round number: ", req.params.roundNumber); + const roundNumber = req.params.roundNumber; + const taskResult = await task(Number(roundNumber)); + res.status(200).json({ result: taskResult }); + }); + app.get("/audit/:roundNumber/:cid/:submitterPublicKey", async (req, res) => { + const cid = req.params.cid; + const roundNumber = req.params.roundNumber; + const submitterPublicKey = req.params.submitterPublicKey; + const auditResult = await audit(cid, Number(roundNumber), submitterPublicKey); + res.status(200).json({ result: auditResult }); + }); + app.get("/submission/:roundNumber", async (req, res) => { + const roundNumber = req.params.roundNumber; + const submissionResult = await submission(Number(roundNumber)); + res.status(200).json({ result: submissionResult }); + }); + + app.get("/submitDistribution/:roundNumber", async (req, res) => { + const roundNumber = req.params.roundNumber; + const submitDistributionResult = await taskRunner.submitDistributionList(Number(roundNumber)); + res.status(200).json({ result: submitDistributionResult }); + }); + +} diff --git a/worker/src/utils/anthropicCheck.ts b/worker/src/utils/anthropicCheck.ts new file mode 100644 index 0000000..0cc627a --- /dev/null +++ b/worker/src/utils/anthropicCheck.ts @@ -0,0 +1,36 @@ +export function isValidAnthropicApiKey(key: string) { + const regex = /^sk-ant-[a-zA-Z0-9_-]{32,}$/; + return regex.test(key); +} + +export async function checkAnthropicAPIKey(apiKey: string) { + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: 'claude-3-opus-20240229', // or a cheaper model + max_tokens: 1, // minimal usage + messages: [{ role: 'user', content: 'Hi' }], + }), + }); + + if (response.status === 200) { + console.log('✅ API key is valid and has credit.'); + return true; + } else { + const data = await response.json().catch(() => ({})); + if (response.status === 401) { + console.log('❌ Invalid API key.'); + } else if (response.status === 403 && data.error?.message?.includes('billing')) { + console.log('❌ API key has no credit or is not authorized.'); + } else { + console.log('⚠️ Unexpected error:', data); + } + return false; + } +} + diff --git a/worker/src/utils/constant.ts b/worker/src/utils/constant.ts new file mode 100644 index 0000000..7d08d5b --- /dev/null +++ b/worker/src/utils/constant.ts @@ -0,0 +1,57 @@ +import dotenv from "dotenv"; +dotenv.config(); + + +export const status = { + ISSUE_FAILED_TO_BE_SUMMARIZED: "Issue failed to be summarized", + ISSUE_SUCCESSFULLY_SUMMARIZED: "Issue successfully summarized", + NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "No issues pending to be summarized", + ROUND_LESS_THAN_OR_EQUAL_TO_1: "Round <= 1", + NO_ORCA_CLIENT: "No orca client", + NO_CHOSEN_AS_ISSUE_SUMMARIZER: "No chosen as issue summarizer", + UNKNOWN_ERROR: "Unknown error", + STAR_ISSUE_FAILED: "Star issue failed", + GITHUB_CHECK_FAILED: "GitHub check failed", + ANTHROPIC_API_KEY_INVALID: "Anthropic API key invalid", + ANTHROPIC_API_KEY_NO_CREDIT: "Anthropic API key has no credit", + NO_DATA_FOR_THIS_ROUND: "No data for this round", + ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "Issue failed to add PR to summarizer todo", +} + +export const errorMessage = { + ISSUE_FAILED_TO_BE_SUMMARIZED: "We couldn't summarize this issue. Please try again later.", + ISSUE_SUCCESSFULLY_SUMMARIZED: "The issue was successfully summarized.", + NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "There are no issues waiting to be summarized at this time.", + ROUND_LESS_THAN_OR_EQUAL_TO_1: "This operation requires a round number greater than 1.", + NO_ORCA_CLIENT: "The Orca client is not available.", + NO_CHOSEN_AS_ISSUE_SUMMARIZER: "You haven't been selected as an issue summarizer.", + UNKNOWN_ERROR: "An unexpected error occurred. Please try again later.", + STAR_ISSUE_FAILED: "We couldn't star the issue. Please try again later.", + GITHUB_CHECK_FAILED: "The GitHub check failed. Please verify your GitHub Key.", + ANTHROPIC_API_KEY_INVALID: "The Anthropic API Key is not valid. Please check your API key.", + ANTHROPIC_API_KEY_NO_CREDIT: "Your Anthropic API key has no remaining credits.", + NO_DATA_FOR_THIS_ROUND: "There is no data available for this round.", + ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "We couldn't add the PR to the summarizer todo list.", +} + +export const actionMessage = { + ISSUE_FAILED_TO_BE_SUMMARIZED: "We couldn't summarize this issue. Please try again later.", + ISSUE_SUCCESSFULLY_SUMMARIZED: "The issue was successfully summarized.", + NO_ISSUES_PENDING_TO_BE_SUMMARIZED: "There are no issues waiting to be summarized at this time.", + ROUND_LESS_THAN_OR_EQUAL_TO_1: "This operation requires a round number greater than 1.", + NO_ORCA_CLIENT: "Please click Orca icon to connect your Orca Pod.", + NO_CHOSEN_AS_ISSUE_SUMMARIZER: "You haven't been selected as an issue summarizer.", + UNKNOWN_ERROR: "An unexpected error occurred. Please try again later.", + STAR_ISSUE_FAILED: "We couldn't star the issue. Please try again later.", + GITHUB_CHECK_FAILED: "Please go to the env variable page to update your GitHub Key.", + ANTHROPIC_API_KEY_INVALID: "Please follow the guide under task description page to set up your Anthropic API key correctly.", + ANTHROPIC_API_KEY_NO_CREDIT: "Please add credits to continue.", + NO_DATA_FOR_THIS_ROUND: "There is no data available for this round.", + ISSUE_FAILED_TO_ADD_PR_TO_SUMMARIZER_TODO: "We couldn't add the PR to the summarizer todo list. Please try again later.", +} +/*********************THE CONSTANTS THAT PROD/TEST ARE DIFFERENT *********************/ +export const defaultBountyMarkdownFile = "https://raw.githubusercontent.com/koii-network/prometheus-swarm-bounties/master/README.md" + +export const customReward = 400*10**9 // This should be in ROE! + +export const middleServerUrl = "https://ooww84kco0s0cs808w8cg804.dev.koii.network" \ No newline at end of file diff --git a/worker/src/utils/distributionList.ts b/worker/src/utils/distributionList.ts new file mode 100644 index 0000000..17e63b3 --- /dev/null +++ b/worker/src/utils/distributionList.ts @@ -0,0 +1,96 @@ +import { namespaceWrapper } from "@_koii/namespace-wrapper"; +import { getFile } from "./ipfs"; + +/** + * Filter out ineligible nodes from the distribution list + * @param distributionList Raw distribution list from namespace + * @param submissions List of submissions for the round + * @returns Filtered distribution list containing only eligible nodes + */ +async function filterIneligibleNodes( + distributionList: Record, + roundNumber: number, +): Promise> { + const filteredDistributionList: Record = {}; + + if (Object.keys(distributionList).length === 0) { + console.log("Distribution list is empty, skipping filterIneligibleNodes"); + return filteredDistributionList; + } + + const taskSubmissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber); + if (!taskSubmissionInfo) { + console.log("Task submission info is null, skipping filterIneligibleNodes"); + return filteredDistributionList; + } + + const submissions = taskSubmissionInfo.submissions; + + for (const [stakingKey, amount] of Object.entries(distributionList)) { + const numericAmount = amount as number; + + // Skip if amount is zero or negative (failed audit) + if (numericAmount <= 0) { + console.log("Skipping staking key:", stakingKey, "Amount:", numericAmount); + continue; + } + + // Find corresponding submission + const submissionCID = submissions[roundNumber][stakingKey]["submission_value"]; + + const submission = await getFile(submissionCID); + + // Skip if no submission found + if (!submission) { + console.log("No submission found, skipping staking key:", stakingKey); + continue; + } + + const submissionData = JSON.parse(submission); + + console.log("Staking key:", stakingKey, "Submission data:", submissionData); + + const payload = await namespaceWrapper.verifySignature(submissionData.signature, stakingKey); + console.log("Payload:", payload); + + const payloadData = JSON.parse(payload.data || "{}"); + + // Skip if submission has no PR URL or is a dummy submission + if (!payloadData.prUrl || payloadData.prUrl === "none") { + continue; + } + + // Node is eligible, include in filtered list + filteredDistributionList[stakingKey] = payloadData; + } + + console.log("Filtered distribution list:", filteredDistributionList); + + return filteredDistributionList; +} + +export async function getDistributionList(roundNumber: number): Promise | null> { + try { + const taskDistributionInfo = await namespaceWrapper.getTaskDistributionInfo(roundNumber); + if (!taskDistributionInfo) { + console.log("Task distribution info is null, skipping task"); + return null; + } + const distribution = taskDistributionInfo.distribution_rewards_submission[roundNumber]; + const leaderStakingKey = Object.keys(distribution)[0]; + console.log("Fetching distribution list for round", roundNumber, "with leader staking key", leaderStakingKey); + const distributionList = await namespaceWrapper.getDistributionList(leaderStakingKey, roundNumber); + if (!distributionList) { + console.log("Distribution list is null, skipping task"); + return null; + } + console.log("Raw distribution list:", distributionList); + + const parsedDistributionList: Record = JSON.parse(distributionList); + + return await filterIneligibleNodes(parsedDistributionList, roundNumber); + } catch (error) { + console.error("Error fetching distribution list:", error); + return null; + } +} diff --git a/worker/src/utils/existingIssues.ts b/worker/src/utils/existingIssues.ts new file mode 100644 index 0000000..3101d3e --- /dev/null +++ b/worker/src/utils/existingIssues.ts @@ -0,0 +1,161 @@ +import { defaultBountyMarkdownFile } from "./constant"; + +interface BountyIssue { + githubUrl: string; + projectName: string; + bountyTask: string; + description: string; + bountyAmount: string; + bountyType: string; + transactionHash: string; + status: string; +} + +export async function getExistingIssues(): Promise { + try { + // read from the bounty markdown file + // console.log('Fetching markdown file from:', defaultBountyMarkdownFile); + const bountyMarkdownFile = await fetch(defaultBountyMarkdownFile); + const bountyMarkdownFileText = await bountyMarkdownFile.text(); + + // console.log('Raw markdown content:', bountyMarkdownFileText); + + const bountyMarkdownFileLines = bountyMarkdownFileText.split("\n"); + // console.log('Number of lines:', bountyMarkdownFileLines.length); + + const issues: BountyIssue[] = []; + let isTableStarted = false; + + for (const line of bountyMarkdownFileLines) { + // Skip empty lines + if (line.trim() === '') { + // console.log('Skipping empty line'); + continue; + } + + // console.log('Processing line:', line); + + // Skip the title line starting with # + if (line.startsWith('#')) { + // console.log('Found title line:', line); + continue; + } + + // Skip the header and separator lines + if (line.startsWith('|') && line.includes('GitHub URL')) { + //console.log('Found header line'); + continue; + } + if (line.startsWith('|') && line.includes('-----')) { + // console.log('Found separator line'); + continue; + } + + // Process table rows + if (line.startsWith('|')) { + isTableStarted = true; + // Remove first and last | and split by | + const cells = line.slice(1, -1).split('|').map(cell => cell.trim()); + // console.log('Parsed cells:', cells); + + // Extract GitHub URL and name from markdown link format [name](url) + const githubUrlMatch = cells[0].match(/\[(.*?)\]\((.*?)\)/); + // console.log('GitHub URL match:', githubUrlMatch); + + const projectName = githubUrlMatch ? githubUrlMatch[1] : ''; + const githubUrl = githubUrlMatch ? githubUrlMatch[2] : ''; + + const issue: BountyIssue = { + githubUrl, + projectName, + bountyTask: cells[1], + description: cells[3], + bountyAmount: cells[4], + bountyType: cells[5], + transactionHash: cells[6], + status: cells[7] + }; + + // console.log('Created issue object:', issue); + issues.push(issue); + } + } + // Filter all issues with status "Initialized" && Bounty Task is Document & Summarize + console.log('Final parsed issues number:', issues.length); + return issues + } catch (error) { + // console.error('Error processing markdown:', error); + throw error; + } +} + + +export async function getInitializedDocumentSummarizeIssues(issues: BountyIssue[]) { + + return issues.filter(issue => issue.status === "Initialized" && issue.bountyTask === "Document & Summarize"); +} + + +// async function main(){ +// const existingIssues = await getExistingIssues(); +// const transactionHashs = [ +// "51680569890c40efa0f1f891044db219", +// "21a7021da88a4092af014702da7638cb", +// "befcf8d281074e3e934d8947c02ecb6f", +// "a1db701bbda24a45b573e58840d9b31c", +// "4ab503566a1142b1a3a9b406849839c9", +// "7f6fb74e4b6a41b0af805ca3f6c9ea15", +// "878af0d284c7460394b6d6e1090119be", +// "64d90b6f891d4ea385c8f6ad81808103", +// "6f7522b2e2374d4ca4f92bcf1f694bec", +// "e85201ae9ed9417e8c56216bb44cd78b", +// "d2ca259ef6ce4129a786677d919aad24", +// "6ce684318aab4356b76ba64e87b31be7", +// "d94d07647b1b42819d9bf629f5624ae1", +// "60aa8f04dd314c14b30e5ac2957bd9f8", +// "b7e21455e41b4626b5015b7bf39ff190", +// "5e7109ed4dd94373958eda2416337ad3", +// "2d647d3ab2c5465890939315ada47fd7", +// "51ade1ba2f6341e99aa6ec56b1a00f27", +// "a74f5e80238a4582aa444c18e9d5d66f", +// "8390a3143a8445f196a124605e524f3d", +// "26b712f341ca457d86db67ecd841c438", +// "0ec98ba1e7174eef87772df8356bab0d", +// "2737c33bff8c4490b7e5f53a5f5da580", +// "e5b9b714d5694680a56cfa77361f3477", +// "afb1bbbf1c074d28bef5fa216008cd6b", +// "b40da8c53a644a6e898e3314e08c10ea", +// "6a2f743c0497427ea4cd3cadb785b166", +// "ce390111854b4a4b980b5e1e3f7c2f0e", +// "c1b54e7a8dfd40be873051dd64bae5c4", +// "7dcda8e5969c45e08f9a8887d8c39d10", +// "fc11382529644d55b95fc2264e40436f", +// "7c145db039b64edba719e81dd398b37e", +// "c92b4920b25540a692c3b8e12215f0e0", +// "cebbf4e2310d4a11ac44321823ddb373", +// "5ae707005d0e413cb9feb9bdadc1e987", +// "d28f92643c2548338d3e49144bc66afc", +// "bd18484224c24fc786a5171e9d06cd50", +// "f0605ea0f9524572bbe5bf4e72597476", +// "62e6303c57334f72ada393bfa9e7aacc", +// "f4ee9168804c4b01932ac76cc32d1f13", +// "d4a95e2d35db47d28a208309019b1925", +// "014425adc1b8447ab34d7d8104e91cf0" +// ] +// const initializedDocumentSummarizeIssues = existingIssues.filter((issue) => transactionHashs.includes(issue.transactionHash)); +// if (initializedDocumentSummarizeIssues.length == 0) { +// console.log("No issues pending to be summarized"); +// return; +// } +// console.log("Initialized Document & Summarize issues number:", initializedDocumentSummarizeIssues.length); +// } +// async function main() { +// try { +// const existingIssues = await getInitializedDocumentSummarizeIssues(); +// console.log('Initialized Document & Summarize issues number:', existingIssues.length); +// } catch (error) { +// console.error('Error in main:', error); +// } +// } + +// main(); \ No newline at end of file diff --git a/worker/src/utils/githubCheck.ts b/worker/src/utils/githubCheck.ts new file mode 100644 index 0000000..84ea624 --- /dev/null +++ b/worker/src/utils/githubCheck.ts @@ -0,0 +1,36 @@ +export async function checkGitHub(username: string, token: string) { + // 1. Check username + const userRes = await fetch(`https://api.github.com/users/${username}`); + const isUsernameValid = userRes.status === 200; + + // 2. Check token + const tokenRes = await fetch('https://api.github.com/user', { + headers: { + Authorization: `token ${token}`, + }, + }); + const isTokenValid = tokenRes.status === 200; + const isIdentityValid = await checkGitHubIdentity(username, token); + return isIdentityValid&&isUsernameValid&&isTokenValid +} + +async function checkGitHubIdentity(username: string, token: string) { + const res = await fetch('https://api.github.com/user', { + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + }); + + if (res.status !== 200) { + return false + } + + const data = await res.json(); + + if (data.login.toLowerCase() !== username.toLowerCase()) { + return false + } + + return true +} \ No newline at end of file diff --git a/worker/src/utils/ipfs.ts b/worker/src/utils/ipfs.ts new file mode 100644 index 0000000..1e529e4 --- /dev/null +++ b/worker/src/utils/ipfs.ts @@ -0,0 +1,34 @@ +import { namespaceWrapper } from "@_koii/namespace-wrapper"; +import { KoiiStorageClient } from "@_koii/storage-task-sdk"; +import fs from "fs"; + +export async function storeFile(data: any, filename: string = "submission.json"): Promise { + // Create a new instance of the Koii Storage Client + const client = KoiiStorageClient.getInstance({}); + const basePath = await namespaceWrapper.getBasePath(); + try { + // Write the data to a temp file + fs.writeFileSync(`${basePath}/${filename}`, typeof data === "string" ? data : JSON.stringify(data)); + + // Get the user staking account, to be used for signing the upload request + const userStaking = await namespaceWrapper.getSubmitterAccount(); + if (!userStaking) { + throw new Error("No staking keypair found"); + } + + // Upload the file to IPFS and get the CID + const { cid } = await client.uploadFile(`${basePath}/${filename}`, userStaking); + return cid; + } catch (error) { + throw error; + } finally { + // Delete the temp file + fs.unlinkSync(`${basePath}/${filename}`); + } +} + +export async function getFile(cid: string, filename: string = "submission.json"): Promise { + const storageClient = KoiiStorageClient.getInstance({}); + const fileBlob = await storageClient.getFile(cid, filename); + return await fileBlob.text(); +} diff --git a/worker/src/utils/leader.ts b/worker/src/utils/leader.ts new file mode 100644 index 0000000..4e79a09 --- /dev/null +++ b/worker/src/utils/leader.ts @@ -0,0 +1,265 @@ +import { namespaceWrapper, TASK_ID } from "@_koii/namespace-wrapper"; +import { getFile } from "./ipfs"; +import seedrandom from "seedrandom"; + +export async function fetchRoundSubmissionGitHubRepoOwner( + roundNumber: number, + submitterPublicKey: string, +): Promise { + try { + const taskSubmissionInfo = await namespaceWrapper.getTaskSubmissionInfo(roundNumber); + if (!taskSubmissionInfo) { + console.error("NO TASK SUBMISSION INFO"); + return null; + } + const submissions = taskSubmissionInfo.submissions; + // This should only have one round + const lastRound = Object.keys(submissions).pop(); + if (!lastRound) { + return null; + } + const lastRoundSubmissions = submissions[lastRound]; + const lastRoundSubmitterSubmission = lastRoundSubmissions[submitterPublicKey]; + console.log("lastRoundSubmitterSubmission", { lastRoundSubmitterSubmission }); + if (!lastRoundSubmitterSubmission) { + return null; + } + const cid = lastRoundSubmitterSubmission.submission_value; + const submissionString = await getFile(cid); + const submission = JSON.parse(submissionString); + console.log({ submission }); + + // verify the signature of the submission + const signaturePayload = await namespaceWrapper.verifySignature(submission.signature, submitterPublicKey); + + console.log({ signaturePayload }); + + // verify the signature payload + if (signaturePayload.error || !signaturePayload.data) { + console.error("INVALID SIGNATURE"); + return null; + } + const data = JSON.parse(signaturePayload.data); + + if (data.taskId !== TASK_ID || data.stakingKey !== submitterPublicKey) { + console.error("INVALID SIGNATURE DATA"); + return null; + } + if (!data.githubUsername) { + console.error("NO GITHUB USERNAME"); + console.log("data", { data }); + return null; + } + return data.githubUsername; + } catch (error) { + console.error("FETCH LAST ROUND SUBMISSION GITHUB REPO OWNER ERROR:", error); + return null; + } +} + +export async function selectShortestDistance(keys: string[], submitterPublicKey: string): Promise { + let shortestDistance = Infinity; + let closestKey = ""; + for (const key of keys) { + const distance = knnDistance(submitterPublicKey, key); + if (distance < shortestDistance) { + shortestDistance = distance; + closestKey = key; + } + } + return closestKey; +} + +async function getSubmissionInfo(roundNumber: number): Promise { + try { + return await namespaceWrapper.getTaskSubmissionInfo(roundNumber); + } catch (error) { + console.error("GET SUBMISSION INFO ERROR:", error); + return null; + } +} + +function calculatePublicKeyFrequency(submissions: any): Record { + const frequency: Record = {}; + for (const round in submissions) { + for (const publicKey in submissions[round]) { + if (frequency[publicKey]) { + frequency[publicKey]++; + } else { + frequency[publicKey] = 1; + } + } + } + return frequency; +} + +function handleAuditTrigger(submissionAuditTrigger: any): Set { + const auditTriggerKeys = new Set(); + for (const round in submissionAuditTrigger) { + for (const publicKey in submissionAuditTrigger[round]) { + auditTriggerKeys.add(publicKey); + } + } + return auditTriggerKeys; +} + +async function selectLeaderKey( + sortedKeys: string[], + leaderNumber: number, + submitterPublicKey: string, + submissionPublicKeysFrequency: Record, +): Promise { + const topValue = sortedKeys[leaderNumber - 1]; + const count = sortedKeys.filter( + (key) => submissionPublicKeysFrequency[key] >= submissionPublicKeysFrequency[topValue], + ).length; + + if (count >= leaderNumber) { + const rng = seedrandom(String(TASK_ID)); + const guaranteedKeys = sortedKeys.filter( + (key) => submissionPublicKeysFrequency[key] > submissionPublicKeysFrequency[topValue], + ); + const randomKeys = sortedKeys + .filter((key) => submissionPublicKeysFrequency[key] === submissionPublicKeysFrequency[topValue]) + .sort(() => rng() - 0.5) + .slice(0, leaderNumber - guaranteedKeys.length); + const keys = [...guaranteedKeys, ...randomKeys]; + return await selectShortestDistance(keys, submitterPublicKey); + } else { + const keys = sortedKeys.slice(0, leaderNumber); + return await selectShortestDistance(keys, submitterPublicKey); + } +} +export async function getRandomNodes(roundNumber: number, numberOfNodes: number): Promise { + console.log("Getting random nodes for round:", roundNumber, "with number of nodes:", numberOfNodes); + const lastRoundSubmission = await getSubmissionInfo(roundNumber - 1); + console.log("Last round submission:", lastRoundSubmission); + if (!lastRoundSubmission) { + return []; + } + + const lastRoundSubmissions = lastRoundSubmission.submissions; + console.log("Last round submissions:", lastRoundSubmissions); + + // Get the last round number + const lastRound = Object.keys(lastRoundSubmissions).pop(); + if (!lastRound) { + return []; + } + + // Get the submissions for that round + const submissions = lastRoundSubmissions[lastRound]; + console.log("Submissions:", submissions); + const availableKeys = Object.keys(submissions); + console.log("Available keys:", availableKeys); + // If we have fewer submissions than requested nodes, return all available submissions + if (availableKeys.length <= numberOfNodes) { + return availableKeys; + } + + const seed = TASK_ID + roundNumber.toString() || "default" + roundNumber; + const rng = seedrandom(seed); + // Use the keys from the submissions object + const randomKeys = availableKeys.sort(() => rng() - 0.5).slice(0, numberOfNodes); + + console.log("Random keys:", randomKeys); + return randomKeys; +} + +// Helper function that finds the leader for a specific round +async function getLeaderForRound( + roundNumber: number, + maxLeaderNumber: number, + submitterPublicKey: string, +): Promise<{ chosenKey: string | null; leaderNode: string | null }> { + if (roundNumber <= 0) { + return { chosenKey: null, leaderNode: null }; + } + + const submissionPublicKeysFrequency: Record = {}; + const submissionAuditTriggerKeys = new Set(); + + for (let i = 1; i < 5; i++) { + const taskSubmissionInfo = await getSubmissionInfo(roundNumber - i); + console.log({ taskSubmissionInfo }); + if (taskSubmissionInfo) { + const submissions = taskSubmissionInfo.submissions; + const frequency = calculatePublicKeyFrequency(submissions); + Object.assign(submissionPublicKeysFrequency, frequency); + + const auditTriggerKeys = handleAuditTrigger(taskSubmissionInfo.submissions_audit_trigger); + auditTriggerKeys.forEach((key) => submissionAuditTriggerKeys.add(key)); + } + } + + const keysNotInAuditTrigger = Object.keys(submissionPublicKeysFrequency).filter( + (key) => !submissionAuditTriggerKeys.has(key), + ); + const sortedKeys = keysNotInAuditTrigger.sort( + (a, b) => submissionPublicKeysFrequency[b] - submissionPublicKeysFrequency[a], + ); + + console.log({ sortedKeys }); + + let chosenKey = null; + + const leaderNumber = sortedKeys.length < maxLeaderNumber ? sortedKeys.length : maxLeaderNumber; + + chosenKey = await selectLeaderKey(sortedKeys, leaderNumber, submitterPublicKey, submissionPublicKeysFrequency); + + // Find GitHub username for the chosen key + for (let i = 1; i < 5; i++) { + const githubUsername = await fetchRoundSubmissionGitHubRepoOwner(roundNumber - i, chosenKey); + if (githubUsername) { + return { chosenKey, leaderNode: githubUsername }; + } + } + + return { chosenKey, leaderNode: null }; +} + +export async function getLeaderNode({ + roundNumber, + leaderNumber = 5, + submitterPublicKey, +}: { + roundNumber: number; + leaderNumber?: number; + submitterPublicKey: string; +}): Promise<{ isLeader: boolean; leaderNode: string | null }> { + // Find leader for current round + const currentLeader = await getLeaderForRound(roundNumber, leaderNumber, submitterPublicKey); + console.log({ currentLeader }); + + if (currentLeader.chosenKey === submitterPublicKey) { + // If we're the leader, get the leader from 3 rounds ago + const previousLeader = await getLeaderForRound(roundNumber - 3, leaderNumber, submitterPublicKey); + console.log({ previousLeader }); + return { isLeader: true, leaderNode: previousLeader.leaderNode }; + } + + // Not the leader, return the current leader's info + return { isLeader: false, leaderNode: currentLeader.leaderNode }; +} + +function base58ToNumber(char: string): number { + const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + return base58Chars.indexOf(char); +} + +function knnDistance(a: string, b: string): number { + if (a.length !== b.length) { + throw new Error("Strings must be of the same length for KNN distance calculation."); + } + const truncatedA = a.slice(0, 30); + const truncatedB = b.slice(0, 30); + + let distance = 0; + for (let i = 0; i < truncatedA.length; i++) { + const numA = base58ToNumber(truncatedA[i]); + const numB = base58ToNumber(truncatedB[i]); + distance += Math.abs(numA - numB); + } + + return distance; +} diff --git a/worker/src/utils/submissionJSONSignatureDecode.ts b/worker/src/utils/submissionJSONSignatureDecode.ts new file mode 100644 index 0000000..35b4788 --- /dev/null +++ b/worker/src/utils/submissionJSONSignatureDecode.ts @@ -0,0 +1,40 @@ +import { TASK_ID } from "@_koii/namespace-wrapper"; +import { getFile } from "./ipfs"; +import { Submission } from "@_koii/namespace-wrapper/dist/types"; +import { Submitter } from "@_koii/task-manager/dist/types/global"; +import { namespaceWrapper } from "@_koii/namespace-wrapper"; +export async function submissionJSONSignatureDecode({submission_value, submitterPublicKey, roundNumber}: {submission_value: string, submitterPublicKey: string, roundNumber: number}) { + let submissionString; + try { + console.log("Getting file from IPFS", submission_value); + submissionString = await getFile(submission_value); + console.log("submissionString", submissionString); + } catch (error) { + + console.log("error", error); + console.error("INVALID SIGNATURE DATA"); + return null; + } + // verify the signature of the submission + const submission = JSON.parse(submissionString); + console.log("submission", submission); + const signaturePayload = await namespaceWrapper.verifySignature(submission.signature, submitterPublicKey); + if (!signaturePayload.data) { + console.error("INVALID SIGNATURE"); + return null; + } + const data = JSON.parse(signaturePayload.data); + console.log("signaturePayload", signaturePayload); + console.log("data", data); + if ( + data.taskId !== TASK_ID || + data.roundNumber !== roundNumber || + data.stakingKey !== submitterPublicKey || + !data.pubKey || + !data.prUrl + ) { + console.error("INVALID SIGNATURE DATA"); + return null; + } + return data; +} \ No newline at end of file diff --git a/worker/tests/README.md b/worker/tests/README.md new file mode 100644 index 0000000..6e77406 --- /dev/null +++ b/worker/tests/README.md @@ -0,0 +1,68 @@ +# Summarizer Task Tests + +This directory contains end-to-end tests for the summarizer task using the Prometheus test framework. + +## Structure + +``` +tests/ +├── config.yaml # Test configuration +├── workers.json # Worker configuration +├── data/ # Test data +│ ├── todos.json # Sample todo items +│ └── issues.json # Sample issues +├── stages/ # Test stages implementation +├── e2e.py # Test runner script +└── steps.py # Test steps definition +``` + +## Prerequisites + +1. Install the test framework: +```bash +pip install -e test-framework/ +``` + +2. Set up environment variables in `.env`: +``` +ANTHROPIC_API_KEY=your_test_key +GITHUB_USERNAME=your_test_username +GITHUB_TOKEN=your_test_token +``` + +## Running Tests + +To run the tests: + +```bash +python -m tests.e2e +``` + +To force reset databases before running: + +```bash +python -m tests.e2e --reset +``` + +## Test Flow + +1. API Key Validation + - Validates Anthropic API key + +2. GitHub Validation + - Validates GitHub credentials + +3. Todo Management + - Fetches todos for each worker + - Generates summaries + - Submits results + +4. Audit Process + - Workers audit each other's submissions + +## Adding New Tests + +1. Create a new stage in `stages/` +2. Add stage to `stages/__init__.py` +3. Add test step in `steps.py` +4. Update test data in `data/` if needed diff --git a/worker/tests/config.ts b/worker/tests/config.ts new file mode 100644 index 0000000..c7f0510 --- /dev/null +++ b/worker/tests/config.ts @@ -0,0 +1,12 @@ +import "dotenv/config"; + +export const TASK_ID = + process.env.TASK_ID || "BXbYKFdXZhQgEaMFbeShaisQBYG1FD4MiSf9gg4n6mVn"; +export const WEBPACKED_FILE_PATH = + process.env.WEBPACKED_FILE_PATH || "../dist/main.js"; + +const envKeywords = process.env.TEST_KEYWORDS ?? ""; + +export const TEST_KEYWORDS = envKeywords + ? envKeywords.split(",") + : ["TEST", "EZ TESTING"]; diff --git a/worker/tests/config.yaml b/worker/tests/config.yaml new file mode 100644 index 0000000..5b260c8 --- /dev/null +++ b/worker/tests/config.yaml @@ -0,0 +1,16 @@ +task_id: "summarizer" +base_port: 5000 +max_rounds: 3 + +data_dir: data +workers_config: workers.json + +mongodb: + database: summarizer_test + collections: + todos: + data_file: todos.json + required_count: 1 + issues: + data_file: issues.json + required_count: 1 diff --git a/worker/tests/data/issues.json b/worker/tests/data/issues.json new file mode 100644 index 0000000..1fed775 --- /dev/null +++ b/worker/tests/data/issues.json @@ -0,0 +1,16 @@ +[ + { + "taskId": "summarizer", + "githubUrl": "https://github.com/test_owner/test_repo/issues/1", + "title": "Test Issue 1", + "body": "This is a test issue for summarization", + "status": "open" + }, + { + "taskId": "summarizer", + "githubUrl": "https://github.com/test_owner/test_repo/issues/2", + "title": "Test Issue 2", + "body": "This is another test issue for summarization", + "status": "open" + } +] diff --git a/worker/tests/data/todos.json b/worker/tests/data/todos.json new file mode 100644 index 0000000..302cf10 --- /dev/null +++ b/worker/tests/data/todos.json @@ -0,0 +1,20 @@ +[ + { + "taskId": "summarizer", + "roundNumber": 1, + "repo_owner": "test_owner", + "repo_name": "test_repo", + "prUrl": "https://github.com/test_owner/test_repo/pull/1", + "status": "pending", + "stakingKey": "test_key_1" + }, + { + "taskId": "summarizer", + "roundNumber": 1, + "repo_owner": "test_owner", + "repo_name": "test_repo", + "prUrl": "https://github.com/test_owner/test_repo/pull/2", + "status": "pending", + "stakingKey": "test_key_2" + } +] diff --git a/worker/tests/debugger.ts b/worker/tests/debugger.ts new file mode 100644 index 0000000..a68d56a --- /dev/null +++ b/worker/tests/debugger.ts @@ -0,0 +1,112 @@ +import "dotenv/config"; +import os from "os"; +import path from "path"; +import { Connection, PublicKey } from "@_koii/web3.js"; +import { borsh_bpf_js_deserialize } from "./wasm/bincode_js.cjs"; +import { TASK_ID, WEBPACKED_FILE_PATH, TEST_KEYWORDS } from "./config"; + +class Debugger { + /* + Create .env file with following variables or directly input values to be used in live-debugging mode. + */ + static taskID = TASK_ID; + static webpackedFilePath = WEBPACKED_FILE_PATH; + static keywords = TEST_KEYWORDS; + static nodeDir: string; + + static async getConfig() { + Debugger.nodeDir = await this.getNodeDirectory(); + + let destinationPath = "executables/" + (await this.getAuditProgram()) + ".js"; + let logPath = "namespace/" + TASK_ID + "/task.log"; + + console.log("Debugger.nodeDir", Debugger.nodeDir); + + return { + webpackedFilePath: Debugger.webpackedFilePath, + destinationPath: destinationPath, + keywords: Debugger.keywords, + logPath: logPath, + nodeDir: Debugger.nodeDir, + taskID: Debugger.taskID, + }; + } + + static async getNodeDirectory() { + if (Debugger.nodeDir) { + return Debugger.nodeDir; + } + const homeDirectory = os.homedir(); + let nodeDirectory: string; + + switch (os.platform()) { + case "linux": + nodeDirectory = path.join(homeDirectory, ".config", "KOII-Desktop-Node"); + break; + case "darwin": + nodeDirectory = path.join(homeDirectory, "Library", "Application Support", "KOII-Desktop-Node"); + break; + default: + // Windows is the default + nodeDirectory = path.join(homeDirectory, "AppData", "Roaming", "KOII-Desktop-Node"); + } + + return nodeDirectory; + } + + static async getAuditProgram() { + const connection = new Connection("https://mainnet.koii.network"); + const taskId = Debugger.taskID; + const accountInfo = await connection.getAccountInfo(new PublicKey(taskId)); + if (!accountInfo?.data) { + console.log(`${taskId} doesn't contain any distribution list data`); + return null; + } + let data; + const owner = accountInfo.owner.toBase58(); + if (owner === "Koiitask22222222222222222222222222222222222") { + data = JSON.parse(accountInfo.data.toString()); + } else if (owner === "KPLTRVs6jA7QTthuJH2cEmyCEskFbSV2xpZw46cganN") { + const buffer = accountInfo.data; + data = borsh_bpf_js_deserialize(buffer); + data = parseTaskState(data); + } else { + console.error(`Not a valid Task ID ${taskId}`); + return null; + } + + console.log("data.task_audit_program", data.task_audit_program); + return data.task_audit_program; + } +} + +function parseTaskState(taskState: any) { + taskState.stake_list = objectify(taskState.stake_list, true); + taskState.ip_address_list = objectify(taskState.ip_address_list, true); + taskState.distributions_audit_record = objectify(taskState.distributions_audit_record, true); + taskState.distributions_audit_trigger = objectify(taskState.distributions_audit_trigger, true); + taskState.submissions = objectify(taskState.submissions, true); + taskState.submissions_audit_trigger = objectify(taskState.submissions_audit_trigger, true); + taskState.distribution_rewards_submission = objectify(taskState.distribution_rewards_submission, true); + taskState.available_balances = objectify(taskState.available_balances, true); + return taskState; +} + +function objectify(data: any, recursive = false) { + if (data instanceof Map) { + const obj = Object.fromEntries(data); + if (recursive) { + for (const key in obj) { + if (obj[key] instanceof Map) { + obj[key] = objectify(obj[key], true); + } else if (typeof obj[key] === "object" && obj[key] !== null) { + obj[key] = objectify(obj[key], true); + } + } + } + return obj; + } + return data; +} + +export default Debugger; diff --git a/worker/tests/e2e.py b/worker/tests/e2e.py new file mode 100644 index 0000000..7e13a1a --- /dev/null +++ b/worker/tests/e2e.py @@ -0,0 +1,62 @@ +"""End-to-end test for the summarizer task.""" + +from pathlib import Path +from prometheus_test import TestRunner +import dotenv +import argparse +import uuid + +from .steps import steps + +dotenv.load_dotenv() + + +def parse_args(): + parser = argparse.ArgumentParser(description="Run summarizer test sequence") + parser.add_argument( + "--reset", + action="store_true", + help="Force reset of all databases before running tests", + ) + return parser.parse_args() + + +def post_load_callback(db): + """Post-load callback to process MongoDB data after JSON import""" + # Process todos collection + todos = list(db.todos.find({"taskId": runner.config.task_id})) + for todo in todos: + if "uuid" not in todo: + todo["uuid"] = str(uuid.uuid4()) + db.todos.replace_one({"_id": todo["_id"]}, todo) + + # Process issues collection + issues = list(db.issues.find({"taskId": runner.config.task_id})) + for issue in issues: + if "uuid" not in issue: + issue["uuid"] = str(uuid.uuid4()) + db.issues.replace_one({"_id": issue["_id"]}, issue) + + +# Global reference to the test runner +runner = None + + +def main(): + global runner + args = parse_args() + + # Create test runner with config from YAML + base_dir = Path(__file__).parent + runner = TestRunner( + steps=steps, + config_file=base_dir / "config.yaml", + config_overrides={"post_load_callback": post_load_callback}, + ) + + # Run test sequence + runner.run(force_reset=args.reset) + + +if __name__ == "__main__": + main() diff --git a/worker/tests/main.test.ts b/worker/tests/main.test.ts new file mode 100644 index 0000000..32c11a5 --- /dev/null +++ b/worker/tests/main.test.ts @@ -0,0 +1,188 @@ +import { initializeTaskManager, taskRunner } from "@_koii/task-manager"; +import { setup } from "../src/task/0-setup"; +import { task } from "../src/task/1-task"; +import { submission } from "../src/task/2-submission"; +import { audit } from "../src/task/3-audit"; +import { distribution } from "../src/task/4-distribution"; +import { routes } from "../src/task/5-routes"; +import { namespaceWrapper, _server } from "@_koii/task-manager/namespace-wrapper"; +import Joi from "joi"; +import axios from "axios"; +import { Submitter } from "@_koii/task-manager"; +beforeAll(async () => { + await namespaceWrapper.defaultTaskSetup(); + initializeTaskManager({ + setup, + task, + submission, + audit, + distribution, + routes, + }); +}); + +describe("Performing the task", () => { + it("should performs the core logic task", async () => { + const round = 1; + await taskRunner.task(round); + const value = await namespaceWrapper.storeGet("value"); + expect(value).toBeDefined(); + expect(value).not.toBeNull(); + }); + + it("should make the submission to k2 for dummy round 1", async () => { + const round = 1; + await taskRunner.submitTask(round); + const taskState = await namespaceWrapper.getTaskState({}); + const schema = Joi.object() + .pattern( + Joi.string(), + Joi.object().pattern( + Joi.string(), + Joi.object({ + submission_value: Joi.string().required(), + slot: Joi.number().integer().required(), + round: Joi.number().integer().required(), + }), + ), + ) + .required() + .min(1); + const validationResult = schema.validate(taskState?.submissions); + try { + expect(validationResult.error).toBeUndefined(); + } catch (e) { + throw new Error("Submission doesn't exist or is incorrect"); + } + }); + + it("should make an audit on submission", async () => { + const round = 1; + await taskRunner.auditTask(round); + const taskState = await namespaceWrapper.getTaskState({}); + console.log("TASK STATE", taskState); + console.log("audit task", taskState?.submissions_audit_trigger); + const schema = Joi.object() + .pattern( + Joi.string(), + Joi.object().pattern( + Joi.string(), + Joi.object({ + trigger_by: Joi.string().required(), + slot: Joi.number().integer().required(), + votes: Joi.array().required(), + }), + ), + ) + .required(); + const validationResult = schema.validate(taskState?.submissions_audit_trigger); + try { + expect(validationResult.error).toBeUndefined(); + } catch (e) { + throw new Error("Submission audit is incorrect"); + } + }); + it("should make the distribution submission to k2 for dummy round 1", async () => { + const round = 1; + await taskRunner.submitDistributionList(round); + + const taskState = await namespaceWrapper.getTaskState({}); + const schema = Joi.object() + .pattern( + Joi.string(), + Joi.object().pattern( + Joi.string(), + Joi.object({ + submission_value: Joi.string().required(), + slot: Joi.number().integer().required(), + round: Joi.number().integer().required(), + }), + ), + ) + .required() + .min(1); + console.log("Distribution submission", taskState?.distribution_rewards_submission); + const validationResult = schema.validate(taskState?.distribution_rewards_submission); + try { + expect(validationResult.error).toBeUndefined(); + } catch (e) { + throw new Error("Distribution submission doesn't exist or is incorrect"); + } + }); + it("should make an audit on distribution submission", async () => { + const round = 1; + await taskRunner.auditDistribution(round); + const taskState = await namespaceWrapper.getTaskState({}); + console.log("audit task", taskState?.distributions_audit_trigger); + const schema = Joi.object() + .pattern( + Joi.string(), + Joi.object().pattern( + Joi.string(), + Joi.object({ + trigger_by: Joi.string().required(), + slot: Joi.number().integer().required(), + votes: Joi.array().required(), + }), + ), + ) + .required(); + const validationResult = schema.validate(taskState?.distributions_audit_trigger); + try { + expect(validationResult.error).toBeUndefined(); + } catch (e) { + throw new Error("Distribution audit is incorrect"); + } + }); + + it("should make sure the submitted distribution list is valid", async () => { + const round = 1; + const distributionList = await namespaceWrapper.getDistributionList("", round); + console.log("Generated distribution List", JSON.parse(distributionList.toString())); + const schema = Joi.object().pattern(Joi.string().required(), Joi.number().integer().required()).required(); + const validationResult = schema.validate(JSON.parse(distributionList.toString())); + console.log(validationResult); + try { + expect(validationResult.error).toBeUndefined(); + } catch (e) { + throw new Error("Submitted distribution list is not valid"); + } + }); + + it("should test the endpoint", async () => { + const response = await axios.get("http://localhost:3000"); + expect(response.status).toBe(200); + expect(response.data).toEqual({ message: "Running", status: 200 }); + }); + + it("should generate a empty distribution list when submission is 0", async () => { + const submitters: Submitter[] = []; + const bounty = Math.floor(Math.random() * 1e15) + 1; + const roundNumber = Math.floor(Math.random() * 1e5) + 1; + const distributionList = await distribution(submitters, bounty, roundNumber); + expect(distributionList).toEqual({}); + }); + + it("should generate a distribution list contains all the submitters", async () => { + const simulatedSubmitters = 5; + const submitters: Submitter[] = []; + // 10k is the rough maximum number of submitters + for (let i = 0; i < simulatedSubmitters; i++) { + const publicKey = `mockPublicKey${i}`; + submitters.push({ + publicKey, + votes: Math.floor(Math.random() * simulatedSubmitters) - 5000, + stake: Math.floor(Math.random() * 1e9) + 1, + }); + } + const bounty = Math.floor(Math.random() * 1e15) + 1; + const roundNumber = 1; + const distributionList = await distribution(submitters, bounty, roundNumber); + expect(Object.keys(distributionList).length).toBe(submitters.length); + expect(Object.keys(distributionList).sort()).toEqual(submitters.map((submitter) => submitter.publicKey).sort()); + }); +}); + +afterAll(async () => { + _server.close(); +}); diff --git a/worker/tests/prod-debug.ts b/worker/tests/prod-debug.ts new file mode 100644 index 0000000..ef1166c --- /dev/null +++ b/worker/tests/prod-debug.ts @@ -0,0 +1,110 @@ +import { spawn } from "cross-spawn"; +import fs from "fs"; +import Debugger from "./debugger"; +import { Tail } from "tail"; +import path from "path"; +import chalk from "chalk"; +import dotenv from "dotenv"; + +dotenv.config(); + +function startWatching(): void { + console.log("Watching for file changes..."); + // watch and trigger builds + build(); +} + +/* build and webpack the task */ +function build(): void { + console.log("Building..."); + const child = spawn("npm", ["run", "webpack:test"], { + stdio: "inherit", + }); + + child.on("close", (code: number) => { + if (code !== 0) { + console.error("Build failed"); + } else { + console.log("Build successful"); + copyWebpackedFile(); + } + return; + }); +} + +/* copy the task to the Desktop Node runtime folder */ +async function copyWebpackedFile(): Promise { + const debugConfig = await Debugger.getConfig(); + console.log("debugConfig", debugConfig); + const nodeDIR = debugConfig.nodeDir; + const sourcePath = path.join(__dirname, debugConfig.webpackedFilePath); + const desktopNodeExecutablePath = path.join(nodeDIR, debugConfig.destinationPath); + const desktopNodeLogPath = path.join(nodeDIR, debugConfig.logPath); + const keywords = debugConfig.keywords; + const taskID = debugConfig.taskID; + + if (!sourcePath || !desktopNodeExecutablePath) { + console.error("Source path or destination path not specified in .env"); + return; + } + + console.log(`Copying webpacked file from ${sourcePath} to ${desktopNodeExecutablePath}...`); + + fs.copyFile(sourcePath, desktopNodeExecutablePath, async (err) => { + if (err) { + console.error("Error copying file:", err); + } else { + console.log("File copied successfully"); + tailLogs(desktopNodeLogPath, keywords, taskID); + } + }); +} + +/* tail logs */ +async function tailLogs(desktopNodeLogPath: string, keywords: string[], taskID: string): Promise { + console.log("Watching logs for messages containing ", keywords); + + // Extract the directory path from the full log file path + const dirPath = path.dirname(desktopNodeLogPath); + + // Check if the directory exists, create it if it doesn't + try { + await fs.promises.access(dirPath, fs.constants.F_OK); + } catch (dirErr) { + console.log( + "Unable to find task directory. Please make sure you have the correct task ID set in your .env file, and run the task on the Desktop Node before running prod-debug.", + ); + process.exit(1); + } + + // Ensure the log file exists, or create it if it doesn't + try { + await fs.promises.access(desktopNodeLogPath, fs.constants.F_OK); + } catch (err) { + console.log(`Log file not found, creating ${desktopNodeLogPath}`); + await fs.promises.writeFile(desktopNodeLogPath, "", { flag: "a" }); // 'a' flag ensures the file is created if it doesn't exist and not overwritten if it exists + } + + let tail = new Tail(desktopNodeLogPath, { + separator: "\n", + flushAtEOF: true, + }); + + console.warn( + `Now watching logs for messages containing ${keywords.join(",")}. Please start the task ${taskID} and keep it running on the Desktop Node.`, + ); + + tail.on("line", (data: string) => { + if (keywords.some((keyword) => data.includes(keyword))) { + console.log(chalk.magenta(data)); + } else { + console.log(data); + } + }); + + +tail.on("error", (error: Error) => { + console.log("ERROR: ", error); + }); +} + +startWatching(); diff --git a/worker/tests/simulateTask.ts b/worker/tests/simulateTask.ts new file mode 100644 index 0000000..b1d0f23 --- /dev/null +++ b/worker/tests/simulateTask.ts @@ -0,0 +1,84 @@ +import { taskRunner } from "@_koii/task-manager"; + +import "../src/index.js"; +import { namespaceWrapper } from "@_koii/task-manager/namespace-wrapper"; +import { Keypair } from "@_koii/web3.js"; + +const numRounds = parseInt(process.argv[2]) || 1; +const roundDelay = parseInt(process.argv[3]) || 5000; +const functionDelay = parseInt(process.argv[4]) || 1000; + +let TASK_TIMES: number[] = []; +let SUBMISSION_TIMES: number[] = []; +let AUDIT_TIMES: number[] = []; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function executeTasks() { + const keypair = Keypair.generate(); + await namespaceWrapper.stakeOnChain(keypair.publicKey, keypair, keypair.publicKey, 10000); + for (let round = 0; round < numRounds; round++) { + const taskStartTime = Date.now(); + await taskRunner.task(round); + const taskEndTime = Date.now(); + TASK_TIMES.push(taskEndTime - taskStartTime); + await sleep(functionDelay); + + const taskSubmissionStartTime = Date.now(); + await taskRunner.submitTask(round); + const taskSubmissionEndTime = Date.now(); + SUBMISSION_TIMES.push(taskSubmissionEndTime - taskSubmissionStartTime); + await sleep(functionDelay); + + const auditStartTime = Date.now(); + await taskRunner.auditTask(round); + const auditEndTime = Date.now(); + AUDIT_TIMES.push(auditEndTime - auditStartTime); + await sleep(functionDelay); + + await taskRunner.selectAndGenerateDistributionList(round); + await sleep(functionDelay); + + await taskRunner.auditDistribution(round); + + if (round < numRounds - 1) { + await sleep(roundDelay); + } + } + console.log("TIME METRICS BELOW"); + function metrics(name: string, times: number[]) { + const average = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length; + const formatTime = (ms: number) => (ms / 1000).toFixed(4); + const formatSlot = (ms: number) => Math.ceil(ms / 408); + const min = Math.min(...times); + const max = Math.max(...times); + const avg = average(times); + const timeMin = formatTime(min); + const timeMax = formatTime(max); + const timeAvg = formatTime(avg); + const slotMin = formatSlot(min); + const slotMax = formatSlot(max); + const slotAvg = formatSlot(avg); + + return { + Metric: `SIMULATED ${name} WINDOW`, + "Avg Time (s)": timeAvg, + "Avg Slots": slotAvg, + "Min Time (s)": timeMin, + "Min Slots": slotMin, + "Max Time (s)": timeMax, + "Max Slots": slotMax, + }; + } + const timeMetrics = metrics("TASK", TASK_TIMES); + const submissionMetrics = metrics("SUBMISSION", SUBMISSION_TIMES); + const auditMetrics = metrics("AUDIT", AUDIT_TIMES); + + console.table([timeMetrics, submissionMetrics, auditMetrics]); + + console.log("All tasks executed. Test completed."); + process.exit(0); +} +setTimeout(executeTasks, 1500); diff --git a/worker/tests/stages/__init__.py b/worker/tests/stages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/worker/tests/stages/audit_summary.py b/worker/tests/stages/audit_summary.py new file mode 100644 index 0000000..0831368 --- /dev/null +++ b/worker/tests/stages/audit_summary.py @@ -0,0 +1,51 @@ +"""Test stage for auditing summary.""" + +import requests +from prometheus_test import Context + + +async def prepare(context: Context, target_name: str): + """Prepare for auditing summary.""" + staking_key = context.env.get("WORKER_ID") + target_submission = await context.storeGet(f"submission-{target_name}") + + return { + "staking_key": staking_key, + "round_number": context.round_number, + "target_submission": target_submission, + "target_name": target_name, + } + + +async def execute(context: Context, prepare_data: dict): + """Execute summary audit test.""" + staking_key = prepare_data["staking_key"] + round_number = prepare_data["round_number"] + target_submission = prepare_data["target_submission"] + target_name = prepare_data["target_name"] + + # Mock response for audit + response = requests.post( + "http://localhost:5000/api/builder/summarizer/audit", + json={ + "taskId": context.config.task_id, + "roundNumber": round_number, + "stakingKey": staking_key, + "submitterKey": target_name, + "cid": target_submission.get("cid"), + "prUrl": target_submission.get("pr_url"), + "githubUsername": target_submission.get("github_username"), + }, + ) + + if response.status_code != 200: + raise Exception(f"Failed to audit summary: {response.text}") + + result = response.json() + if not result.get("success"): + raise Exception("Failed to audit summary") + + # Store audit result + await context.storeSet(f"audit-{staking_key}-{target_name}", result.get("data")) + + return True diff --git a/worker/tests/stages/fetch_summarizer_todo.py b/worker/tests/stages/fetch_summarizer_todo.py new file mode 100644 index 0000000..75dedfe --- /dev/null +++ b/worker/tests/stages/fetch_summarizer_todo.py @@ -0,0 +1,39 @@ +"""Test stage for fetching summarizer todo.""" + +import requests +from prometheus_test import Context + + +async def prepare(context: Context): + """Prepare for fetching summarizer todo.""" + return { + "staking_key": context.env.get("WORKER_ID"), + "round_number": context.round_number, + } + + +async def execute(context: Context, prepare_data: dict): + """Execute fetch summarizer todo test.""" + staking_key = prepare_data["staking_key"] + round_number = prepare_data["round_number"] + + # Mock response for fetching todo + response = requests.post( + "http://localhost:5000/api/builder/summarizer/fetch-summarizer-todo", + json={ + "stakingKey": staking_key, + "roundNumber": round_number, + }, + ) + + if response.status_code != 200: + raise Exception(f"Failed to fetch summarizer todo: {response.text}") + + result = response.json() + if not result.get("success"): + raise Exception("Failed to fetch summarizer todo") + + # Store todo data for next steps + await context.storeSet(f"todo-{staking_key}", result.get("data")) + + return True diff --git a/worker/tests/stages/generate_summary.py b/worker/tests/stages/generate_summary.py new file mode 100644 index 0000000..7a995e4 --- /dev/null +++ b/worker/tests/stages/generate_summary.py @@ -0,0 +1,47 @@ +"""Test stage for generating repository summary.""" + +import requests +from prometheus_test import Context + + +async def prepare(context: Context): + """Prepare for generating summary.""" + staking_key = context.env.get("WORKER_ID") + todo = await context.storeGet(f"todo-{staking_key}") + + return { + "staking_key": staking_key, + "round_number": context.round_number, + "repo_owner": todo.get("repo_owner"), + "repo_name": todo.get("repo_name"), + } + + +async def execute(context: Context, prepare_data: dict): + """Execute summary generation test.""" + staking_key = prepare_data["staking_key"] + round_number = prepare_data["round_number"] + repo_owner = prepare_data["repo_owner"] + repo_name = prepare_data["repo_name"] + + # Mock response for repo summary generation + response = requests.post( + "http://localhost:5000/api/builder/summarizer/generate-summary", + json={ + "taskId": context.config.task_id, + "round_number": str(round_number), + "repo_url": f"https://github.com/{repo_owner}/{repo_name}", + }, + ) + + if response.status_code != 200: + raise Exception(f"Failed to generate summary: {response.text}") + + result = response.json() + if not result.get("success"): + raise Exception("Failed to generate summary") + + # Store PR URL for next steps + await context.storeSet(f"pr-{staking_key}", result.get("data", {}).get("pr_url")) + + return True diff --git a/worker/tests/stages/submit_summary.py b/worker/tests/stages/submit_summary.py new file mode 100644 index 0000000..3e76fa5 --- /dev/null +++ b/worker/tests/stages/submit_summary.py @@ -0,0 +1,56 @@ +"""Test stage for submitting summary.""" + +import requests +from prometheus_test import Context + + +async def prepare(context: Context): + """Prepare for submitting summary.""" + staking_key = context.env.get("WORKER_ID") + pr_url = await context.storeGet(f"pr-{staking_key}") + + return { + "staking_key": staking_key, + "round_number": context.round_number, + "pr_url": pr_url, + "github_username": context.env.get("GITHUB_USERNAME"), + } + + +async def execute(context: Context, prepare_data: dict): + """Execute summary submission test.""" + staking_key = prepare_data["staking_key"] + round_number = prepare_data["round_number"] + pr_url = prepare_data["pr_url"] + github_username = prepare_data["github_username"] + + # Mock response for submission + response = requests.post( + "http://localhost:5000/api/builder/summarizer/submit", + json={ + "taskId": context.config.task_id, + "roundNumber": round_number, + "prUrl": pr_url, + "stakingKey": staking_key, + "githubUsername": github_username, + }, + ) + + if response.status_code != 200: + raise Exception(f"Failed to submit summary: {response.text}") + + result = response.json() + if not result.get("success"): + raise Exception("Failed to submit summary") + + # Store submission data for audit + await context.storeSet( + f"submission-{staking_key}", + { + "cid": result.get("data", {}).get("cid"), + "pr_url": pr_url, + "github_username": github_username, + }, + ) + + return True diff --git a/worker/tests/stages/validate_api_keys.py b/worker/tests/stages/validate_api_keys.py new file mode 100644 index 0000000..92913d1 --- /dev/null +++ b/worker/tests/stages/validate_api_keys.py @@ -0,0 +1,31 @@ +"""Test stage for validating API keys.""" + +import requests +from prometheus_test import Context + + +async def prepare(context: Context): + """Prepare for API key validation test.""" + return { + "api_key": context.env.get("ANTHROPIC_API_KEY"), + } + + +async def execute(context: Context, prepare_data: dict): + """Execute API key validation test.""" + api_key = prepare_data["api_key"] + + # Mock response for Anthropic API validation + response = requests.post( + "http://localhost:5000/api/builder/summarizer/validate-api-key", + json={"api_key": api_key}, + ) + + if response.status_code != 200: + raise Exception(f"API key validation failed: {response.text}") + + result = response.json() + if not result.get("valid"): + raise Exception("API key is not valid") + + return True diff --git a/worker/tests/stages/validate_github.py b/worker/tests/stages/validate_github.py new file mode 100644 index 0000000..60c67a4 --- /dev/null +++ b/worker/tests/stages/validate_github.py @@ -0,0 +1,33 @@ +"""Test stage for validating GitHub credentials.""" + +import requests +from prometheus_test import Context + + +async def prepare(context: Context): + """Prepare for GitHub validation test.""" + return { + "github_username": context.env.get("GITHUB_USERNAME"), + "github_token": context.env.get("GITHUB_TOKEN"), + } + + +async def execute(context: Context, prepare_data: dict): + """Execute GitHub validation test.""" + username = prepare_data["github_username"] + token = prepare_data["github_token"] + + # Mock response for GitHub validation + response = requests.post( + "http://localhost:5000/api/builder/summarizer/validate-github", + json={"username": username, "token": token}, + ) + + if response.status_code != 200: + raise Exception(f"GitHub validation failed: {response.text}") + + result = response.json() + if not result.get("valid"): + raise Exception("GitHub credentials are not valid") + + return True diff --git a/worker/tests/steps.py b/worker/tests/steps.py new file mode 100644 index 0000000..f9520c1 --- /dev/null +++ b/worker/tests/steps.py @@ -0,0 +1,85 @@ +"""Test step definitions.""" + +from prometheus_test import TestStep +from functools import partial +from .stages import ( + validate_api_keys, + validate_github, + fetch_summarizer_todo, + generate_summary, + submit_summary, + audit_summary, +) + +steps = [ + TestStep( + name="validate_api_keys", + description="Validate Anthropic API key", + prepare=validate_api_keys.prepare, + execute=validate_api_keys.execute, + worker="worker1", + ), + TestStep( + name="validate_github", + description="Validate GitHub credentials", + prepare=validate_github.prepare, + execute=validate_github.execute, + worker="worker1", + ), + TestStep( + name="fetch_todo_worker1", + description="Fetch summarizer todo for worker1", + prepare=fetch_summarizer_todo.prepare, + execute=fetch_summarizer_todo.execute, + worker="worker1", + ), + TestStep( + name="fetch_todo_worker2", + description="Fetch summarizer todo for worker2", + prepare=fetch_summarizer_todo.prepare, + execute=fetch_summarizer_todo.execute, + worker="worker2", + ), + TestStep( + name="generate_summary_worker1", + description="Generate summary for worker1's todo", + prepare=generate_summary.prepare, + execute=generate_summary.execute, + worker="worker1", + ), + TestStep( + name="generate_summary_worker2", + description="Generate summary for worker2's todo", + prepare=generate_summary.prepare, + execute=generate_summary.execute, + worker="worker2", + ), + TestStep( + name="submit_summary_worker1", + description="Submit summary for worker1", + prepare=submit_summary.prepare, + execute=submit_summary.execute, + worker="worker1", + ), + TestStep( + name="submit_summary_worker2", + description="Submit summary for worker2", + prepare=submit_summary.prepare, + execute=submit_summary.execute, + worker="worker2", + ), + TestStep( + name="audit_worker1", + description="Worker1 audits Worker2's submission", + prepare=partial(audit_summary.prepare, target_name="worker2"), + execute=audit_summary.execute, + worker="worker1", + ), + TestStep( + name="audit_worker2", + description="Worker2 audits Worker1's submission", + prepare=partial(audit_summary.prepare, target_name="worker1"), + execute=audit_summary.execute, + worker="worker2", + ), +] diff --git a/worker/tests/test.ts b/worker/tests/test.ts new file mode 100644 index 0000000..3f3d96f --- /dev/null +++ b/worker/tests/test.ts @@ -0,0 +1,19 @@ +// async function testSlackWebhook(){ +// const slackResponse = await fetch('https://hooks.slack.com/services/', { +// method: "POST", +// headers: { +// "Content-Type": "application/json", +// }, +// body: JSON.stringify({ +// text: `[TASK] Error summarizing issue:\n ${JSON.stringify({ +// status: "error", +// data: { +// message: "test" +// } +// })}` +// }), +// }); +// console.log("[TASK] slackResponse: ", slackResponse); +// } + +// testSlackWebhook(); \ No newline at end of file diff --git a/worker/tests/wasm/bincode_js.cjs b/worker/tests/wasm/bincode_js.cjs new file mode 100644 index 0000000..d4857f9 --- /dev/null +++ b/worker/tests/wasm/bincode_js.cjs @@ -0,0 +1,1211 @@ +let imports = {}; +imports['__wbindgen_placeholder__'] = module.exports; +let wasm; +const { TextDecoder, TextEncoder } = require(`util`); + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachedUint8Memory0 = null; + +function getUint8Memory0() { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function getObject(idx) { return heap[idx]; } + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedFloat64Memory0 = null; + +function getFloat64Memory0() { + if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) { + cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer); + } + return cachedFloat64Memory0; +} + +let cachedInt32Memory0 = null; + +function getInt32Memory0() { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +let WASM_VECTOR_LEN = 0; + +let cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} +/** +* @param {any} val +* @returns {any} +*/ +module.exports.bincode_js_deserialize = function(val) { + const ret = wasm.bincode_js_deserialize(addHeapObject(val)); + return takeObject(ret); +}; + +/** +* @param {any} val +* @returns {any} +*/ +module.exports.borsh_bpf_js_deserialize = function(val) { + const ret = wasm.borsh_bpf_js_deserialize(addHeapObject(val)); + return takeObject(ret); +}; + +/** +* Initialize Javascript logging and panic handler +*/ +module.exports.solana_program_init = function() { + wasm.solana_program_init(); +}; + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } + return instance.ptr; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedUint32Memory0 = null; + +function getUint32Memory0() { + if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) { + cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32Memory0; +} + +function passArrayJsValueToWasm0(array, malloc) { + const ptr = malloc(array.length * 4, 4) >>> 0; + const mem = getUint32Memory0(); + for (let i = 0; i < array.length; i++) { + mem[ptr / 4 + i] = addHeapObject(array[i]); + } + WASM_VECTOR_LEN = array.length; + return ptr; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +/** +* A hash; the 32-byte output of a hashing algorithm. +* +* This struct is used most often in `solana-sdk` and related crates to contain +* a [SHA-256] hash, but may instead contain a [blake3] hash, as created by the +* [`blake3`] module (and used in [`Message::hash`]). +* +* [SHA-256]: https://en.wikipedia.org/wiki/SHA-2 +* [blake3]: https://github.com/BLAKE3-team/BLAKE3 +* [`blake3`]: crate::blake3 +* [`Message::hash`]: crate::message::Message::hash +*/ +class Hash { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Hash.prototype); + obj.__wbg_ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_hash_free(ptr); + } + /** + * Create a new Hash object + * + * * `value` - optional hash as a base58 encoded string, `Uint8Array`, `[number]` + * @param {any} value + */ + constructor(value) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.hash_constructor(retptr, addHeapObject(value)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return Hash.__wrap(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Return the base58 string representation of the hash + * @returns {string} + */ + toString() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.hash_toString(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + } + /** + * Checks if two `Hash`s are equal + * @param {Hash} other + * @returns {boolean} + */ + equals(other) { + _assertClass(other, Hash); + const ret = wasm.hash_equals(this.__wbg_ptr, other.__wbg_ptr); + return ret !== 0; + } + /** + * Return the `Uint8Array` representation of the hash + * @returns {Uint8Array} + */ + toBytes() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.hash_toBytes(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v1 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v1; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} +module.exports.Hash = Hash; +/** +* A directive for a single invocation of a Solana program. +* +* An instruction specifies which program it is calling, which accounts it may +* read or modify, and additional data that serves as input to the program. One +* or more instructions are included in transactions submitted by Solana +* clients. Instructions are also used to describe [cross-program +* invocations][cpi]. +* +* [cpi]: https://docs.solana.com/developing/programming-model/calling-between-programs +* +* During execution, a program will receive a list of account data as one of +* its arguments, in the same order as specified during `Instruction` +* construction. +* +* While Solana is agnostic to the format of the instruction data, it has +* built-in support for serialization via [`borsh`] and [`bincode`]. +* +* [`borsh`]: https://docs.rs/borsh/latest/borsh/ +* [`bincode`]: https://docs.rs/bincode/latest/bincode/ +* +* # Specifying account metadata +* +* When constructing an [`Instruction`], a list of all accounts that may be +* read or written during the execution of that instruction must be supplied as +* [`AccountMeta`] values. +* +* Any account whose data may be mutated by the program during execution must +* be specified as writable. During execution, writing to an account that was +* not specified as writable will cause the transaction to fail. Writing to an +* account that is not owned by the program will cause the transaction to fail. +* +* Any account whose lamport balance may be mutated by the program during +* execution must be specified as writable. During execution, mutating the +* lamports of an account that was not specified as writable will cause the +* transaction to fail. While _subtracting_ lamports from an account not owned +* by the program will cause the transaction to fail, _adding_ lamports to any +* account is allowed, as long is it is mutable. +* +* Accounts that are not read or written by the program may still be specified +* in an `Instruction`'s account list. These will affect scheduling of program +* execution by the runtime, but will otherwise be ignored. +* +* When building a transaction, the Solana runtime coalesces all accounts used +* by all instructions in that transaction, along with accounts and permissions +* required by the runtime, into a single account list. Some accounts and +* account permissions required by the runtime to process a transaction are +* _not_ required to be included in an `Instruction`s account list. These +* include: +* +* - The program ID — it is a separate field of `Instruction` +* - The transaction's fee-paying account — it is added during [`Message`] +* construction. A program may still require the fee payer as part of the +* account list if it directly references it. +* +* [`Message`]: crate::message::Message +* +* Programs may require signatures from some accounts, in which case they +* should be specified as signers during `Instruction` construction. The +* program must still validate during execution that the account is a signer. +*/ +class Instruction { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Instruction.prototype); + obj.__wbg_ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_instruction_free(ptr); + } +} +module.exports.Instruction = Instruction; +/** +*/ +class Instructions { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Instructions.prototype); + obj.__wbg_ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_instructions_free(ptr); + } + /** + */ + constructor() { + const ret = wasm.instructions_constructor(); + return Instructions.__wrap(ret); + } + /** + * @param {Instruction} instruction + */ + push(instruction) { + _assertClass(instruction, Instruction); + var ptr0 = instruction.__destroy_into_raw(); + wasm.instructions_push(this.__wbg_ptr, ptr0); + } +} +module.exports.Instructions = Instructions; +/** +* A Solana transaction message (legacy). +* +* See the [`message`] module documentation for further description. +* +* [`message`]: crate::message +* +* Some constructors accept an optional `payer`, the account responsible for +* paying the cost of executing a transaction. In most cases, callers should +* specify the payer explicitly in these constructors. In some cases though, +* the caller is not _required_ to specify the payer, but is still allowed to: +* in the `Message` structure, the first account is always the fee-payer, so if +* the caller has knowledge that the first account of the constructed +* transaction's `Message` is both a signer and the expected fee-payer, then +* redundantly specifying the fee-payer is not strictly required. +*/ +class Message { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_message_free(ptr); + } + /** + * The id of a recent ledger entry. + * @returns {Hash} + */ + get recent_blockhash() { + const ret = wasm.__wbg_get_message_recent_blockhash(this.__wbg_ptr); + return Hash.__wrap(ret); + } + /** + * The id of a recent ledger entry. + * @param {Hash} arg0 + */ + set recent_blockhash(arg0) { + _assertClass(arg0, Hash); + var ptr0 = arg0.__destroy_into_raw(); + wasm.__wbg_set_message_recent_blockhash(this.__wbg_ptr, ptr0); + } +} +module.exports.Message = Message; +/** +* The address of a [Solana account][acc]. +* +* Some account addresses are [ed25519] public keys, with corresponding secret +* keys that are managed off-chain. Often, though, account addresses do not +* have corresponding secret keys — as with [_program derived +* addresses_][pdas] — or the secret key is not relevant to the operation +* of a program, and may have even been disposed of. As running Solana programs +* can not safely create or manage secret keys, the full [`Keypair`] is not +* defined in `solana-program` but in `solana-sdk`. +* +* [acc]: https://docs.solana.com/developing/programming-model/accounts +* [ed25519]: https://ed25519.cr.yp.to/ +* [pdas]: https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses +* [`Keypair`]: https://docs.rs/solana-sdk/latest/solana_sdk/signer/keypair/struct.Keypair.html +*/ +class Pubkey { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(Pubkey.prototype); + obj.__wbg_ptr = ptr; + + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_pubkey_free(ptr); + } + /** + * Create a new Pubkey object + * + * * `value` - optional public key as a base58 encoded string, `Uint8Array`, `[number]` + * @param {any} value + */ + constructor(value) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.pubkey_constructor(retptr, addHeapObject(value)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return Pubkey.__wrap(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Return the base58 string representation of the public key + * @returns {string} + */ + toString() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.pubkey_toString(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(deferred1_0, deferred1_1, 1); + } + } + /** + * Check if a `Pubkey` is on the ed25519 curve. + * @returns {boolean} + */ + isOnCurve() { + const ret = wasm.pubkey_isOnCurve(this.__wbg_ptr); + return ret !== 0; + } + /** + * Checks if two `Pubkey`s are equal + * @param {Pubkey} other + * @returns {boolean} + */ + equals(other) { + _assertClass(other, Pubkey); + const ret = wasm.pubkey_equals(this.__wbg_ptr, other.__wbg_ptr); + return ret !== 0; + } + /** + * Return the `Uint8Array` representation of the public key + * @returns {Uint8Array} + */ + toBytes() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.pubkey_toBytes(retptr, this.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v1 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v1; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Derive a Pubkey from another Pubkey, string seed, and a program id + * @param {Pubkey} base + * @param {string} seed + * @param {Pubkey} owner + * @returns {Pubkey} + */ + static createWithSeed(base, seed, owner) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + _assertClass(base, Pubkey); + const ptr0 = passStringToWasm0(seed, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(owner, Pubkey); + wasm.pubkey_createWithSeed(retptr, base.__wbg_ptr, ptr0, len0, owner.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return Pubkey.__wrap(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Derive a program address from seeds and a program id + * @param {any[]} seeds + * @param {Pubkey} program_id + * @returns {Pubkey} + */ + static createProgramAddress(seeds, program_id) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArrayJsValueToWasm0(seeds, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(program_id, Pubkey); + wasm.pubkey_createProgramAddress(retptr, ptr0, len0, program_id.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return Pubkey.__wrap(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Find a valid program address + * + * Returns: + * * `[PubKey, number]` - the program address and bump seed + * @param {any[]} seeds + * @param {Pubkey} program_id + * @returns {any} + */ + static findProgramAddress(seeds, program_id) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArrayJsValueToWasm0(seeds, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(program_id, Pubkey); + wasm.pubkey_findProgramAddress(retptr, ptr0, len0, program_id.__wbg_ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} +module.exports.Pubkey = Pubkey; + +class SystemInstruction { + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_systeminstruction_free(ptr); + } + /** + * @param {Pubkey} from_pubkey + * @param {Pubkey} to_pubkey + * @param {bigint} lamports + * @param {bigint} space + * @param {Pubkey} owner + * @returns {Instruction} + */ + static createAccount(from_pubkey, to_pubkey, lamports, space, owner) { + _assertClass(from_pubkey, Pubkey); + _assertClass(to_pubkey, Pubkey); + _assertClass(owner, Pubkey); + const ret = wasm.systeminstruction_createAccount(from_pubkey.__wbg_ptr, to_pubkey.__wbg_ptr, lamports, space, owner.__wbg_ptr); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} from_pubkey + * @param {Pubkey} to_pubkey + * @param {Pubkey} base + * @param {string} seed + * @param {bigint} lamports + * @param {bigint} space + * @param {Pubkey} owner + * @returns {Instruction} + */ + static createAccountWithSeed(from_pubkey, to_pubkey, base, seed, lamports, space, owner) { + _assertClass(from_pubkey, Pubkey); + _assertClass(to_pubkey, Pubkey); + _assertClass(base, Pubkey); + const ptr0 = passStringToWasm0(seed, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(owner, Pubkey); + const ret = wasm.systeminstruction_createAccountWithSeed(from_pubkey.__wbg_ptr, to_pubkey.__wbg_ptr, base.__wbg_ptr, ptr0, len0, lamports, space, owner.__wbg_ptr); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} pubkey + * @param {Pubkey} owner + * @returns {Instruction} + */ + static assign(pubkey, owner) { + _assertClass(pubkey, Pubkey); + _assertClass(owner, Pubkey); + const ret = wasm.systeminstruction_assign(pubkey.__wbg_ptr, owner.__wbg_ptr); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} pubkey + * @param {Pubkey} base + * @param {string} seed + * @param {Pubkey} owner + * @returns {Instruction} + */ + static assignWithSeed(pubkey, base, seed, owner) { + _assertClass(pubkey, Pubkey); + _assertClass(base, Pubkey); + const ptr0 = passStringToWasm0(seed, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(owner, Pubkey); + const ret = wasm.systeminstruction_assignWithSeed(pubkey.__wbg_ptr, base.__wbg_ptr, ptr0, len0, owner.__wbg_ptr); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} from_pubkey + * @param {Pubkey} to_pubkey + * @param {bigint} lamports + * @returns {Instruction} + */ + static transfer(from_pubkey, to_pubkey, lamports) { + _assertClass(from_pubkey, Pubkey); + _assertClass(to_pubkey, Pubkey); + const ret = wasm.systeminstruction_transfer(from_pubkey.__wbg_ptr, to_pubkey.__wbg_ptr, lamports); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} from_pubkey + * @param {Pubkey} from_base + * @param {string} from_seed + * @param {Pubkey} from_owner + * @param {Pubkey} to_pubkey + * @param {bigint} lamports + * @returns {Instruction} + */ + static transferWithSeed(from_pubkey, from_base, from_seed, from_owner, to_pubkey, lamports) { + _assertClass(from_pubkey, Pubkey); + _assertClass(from_base, Pubkey); + const ptr0 = passStringToWasm0(from_seed, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(from_owner, Pubkey); + _assertClass(to_pubkey, Pubkey); + const ret = wasm.systeminstruction_transferWithSeed(from_pubkey.__wbg_ptr, from_base.__wbg_ptr, ptr0, len0, from_owner.__wbg_ptr, to_pubkey.__wbg_ptr, lamports); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} pubkey + * @param {bigint} space + * @returns {Instruction} + */ + static allocate(pubkey, space) { + _assertClass(pubkey, Pubkey); + const ret = wasm.systeminstruction_allocate(pubkey.__wbg_ptr, space); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} address + * @param {Pubkey} base + * @param {string} seed + * @param {bigint} space + * @param {Pubkey} owner + * @returns {Instruction} + */ + static allocateWithSeed(address, base, seed, space, owner) { + _assertClass(address, Pubkey); + _assertClass(base, Pubkey); + const ptr0 = passStringToWasm0(seed, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + _assertClass(owner, Pubkey); + const ret = wasm.systeminstruction_allocateWithSeed(address.__wbg_ptr, base.__wbg_ptr, ptr0, len0, space, owner.__wbg_ptr); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} from_pubkey + * @param {Pubkey} nonce_pubkey + * @param {Pubkey} authority + * @param {bigint} lamports + * @returns {Array} + */ + static createNonceAccount(from_pubkey, nonce_pubkey, authority, lamports) { + _assertClass(from_pubkey, Pubkey); + _assertClass(nonce_pubkey, Pubkey); + _assertClass(authority, Pubkey); + const ret = wasm.systeminstruction_createNonceAccount(from_pubkey.__wbg_ptr, nonce_pubkey.__wbg_ptr, authority.__wbg_ptr, lamports); + return takeObject(ret); + } + /** + * @param {Pubkey} nonce_pubkey + * @param {Pubkey} authorized_pubkey + * @returns {Instruction} + */ + static advanceNonceAccount(nonce_pubkey, authorized_pubkey) { + _assertClass(nonce_pubkey, Pubkey); + _assertClass(authorized_pubkey, Pubkey); + const ret = wasm.systeminstruction_advanceNonceAccount(nonce_pubkey.__wbg_ptr, authorized_pubkey.__wbg_ptr); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} nonce_pubkey + * @param {Pubkey} authorized_pubkey + * @param {Pubkey} to_pubkey + * @param {bigint} lamports + * @returns {Instruction} + */ + static withdrawNonceAccount(nonce_pubkey, authorized_pubkey, to_pubkey, lamports) { + _assertClass(nonce_pubkey, Pubkey); + _assertClass(authorized_pubkey, Pubkey); + _assertClass(to_pubkey, Pubkey); + const ret = wasm.systeminstruction_withdrawNonceAccount(nonce_pubkey.__wbg_ptr, authorized_pubkey.__wbg_ptr, to_pubkey.__wbg_ptr, lamports); + return Instruction.__wrap(ret); + } + /** + * @param {Pubkey} nonce_pubkey + * @param {Pubkey} authorized_pubkey + * @param {Pubkey} new_authority + * @returns {Instruction} + */ + static authorizeNonceAccount(nonce_pubkey, authorized_pubkey, new_authority) { + _assertClass(nonce_pubkey, Pubkey); + _assertClass(authorized_pubkey, Pubkey); + _assertClass(new_authority, Pubkey); + const ret = wasm.systeminstruction_authorizeNonceAccount(nonce_pubkey.__wbg_ptr, authorized_pubkey.__wbg_ptr, new_authority.__wbg_ptr); + return Instruction.__wrap(ret); + } +} +module.exports.SystemInstruction = SystemInstruction; + +module.exports.__wbindgen_error_new = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); +}; + +module.exports.__wbg_log_fb911463b057a706 = function(arg0, arg1) { + console.log(getStringFromWasm0(arg0, arg1)); +}; + +module.exports.__wbindgen_number_new = function(arg0) { + const ret = arg0; + return addHeapObject(ret); +}; + +module.exports.__wbindgen_bigint_from_u64 = function(arg0) { + const ret = BigInt.asUintN(64, arg0); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_is_object = function(arg0) { + const val = getObject(arg0); + const ret = typeof(val) === 'object' && val !== null; + return ret; +}; + +module.exports.__wbindgen_jsval_loose_eq = function(arg0, arg1) { + const ret = getObject(arg0) == getObject(arg1); + return ret; +}; + +module.exports.__wbindgen_boolean_get = function(arg0) { + const v = getObject(arg0); + const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; + return ret; +}; + +module.exports.__wbindgen_number_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'number' ? obj : undefined; + getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); +}; + +module.exports.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; +}; + +module.exports.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); +}; + +module.exports.__wbg_instruction_new = function(arg0) { + const ret = Instruction.__wrap(arg0); + return addHeapObject(ret); +}; + +module.exports.__wbg_pubkey_new = function(arg0) { + const ret = Pubkey.__wrap(arg0); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_is_undefined = function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; +}; + +module.exports.__wbg_debug_9a6b3243fbbebb61 = function(arg0) { + console.debug(getObject(arg0)); +}; + +module.exports.__wbg_error_788ae33f81d3b84b = function(arg0) { + console.error(getObject(arg0)); +}; + +module.exports.__wbg_info_2e30e8204b29d91d = function(arg0) { + console.info(getObject(arg0)); +}; + +module.exports.__wbg_log_1d3ae0273d8f4f8a = function(arg0) { + console.log(getObject(arg0)); +}; + +module.exports.__wbg_warn_d60e832f9882c1b2 = function(arg0) { + console.warn(getObject(arg0)); +}; + +module.exports.__wbg_new_abda76e883ba8a5f = function() { + const ret = new Error(); + return addHeapObject(ret); +}; + +module.exports.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; +}; + +module.exports.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } +}; + +module.exports.__wbindgen_is_string = function(arg0) { + const ret = typeof(getObject(arg0)) === 'string'; + return ret; +}; + +module.exports.__wbg_get_44be0491f933a435 = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); +}; + +module.exports.__wbg_length_fff51ee6522a1a18 = function(arg0) { + const ret = getObject(arg0).length; + return ret; +}; + +module.exports.__wbg_new_898a68150f225f2e = function() { + const ret = new Array(); + return addHeapObject(ret); +}; + +module.exports.__wbindgen_is_function = function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; +}; + +module.exports.__wbg_new_56693dbed0c32988 = function() { + const ret = new Map(); + return addHeapObject(ret); +}; + +module.exports.__wbg_next_526fc47e980da008 = function(arg0) { + const ret = getObject(arg0).next; + return addHeapObject(ret); +}; + +module.exports.__wbg_next_ddb3312ca1c4e32a = function() { return handleError(function (arg0) { + const ret = getObject(arg0).next(); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_done_5c1f01fb660d73b5 = function(arg0) { + const ret = getObject(arg0).done; + return ret; +}; + +module.exports.__wbg_value_1695675138684bd5 = function(arg0) { + const ret = getObject(arg0).value; + return addHeapObject(ret); +}; + +module.exports.__wbg_iterator_97f0c81209c6c35a = function() { + const ret = Symbol.iterator; + return addHeapObject(ret); +}; + +module.exports.__wbg_get_97b561fb56f034b5 = function() { return handleError(function (arg0, arg1) { + const ret = Reflect.get(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_call_cb65541d95d71282 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_new_b51585de1b234aff = function() { + const ret = new Object(); + return addHeapObject(ret); +}; + +module.exports.__wbg_newwithlength_3ec098a360da1909 = function(arg0) { + const ret = new Array(arg0 >>> 0); + return addHeapObject(ret); +}; + +module.exports.__wbg_set_502d29070ea18557 = function(arg0, arg1, arg2) { + getObject(arg0)[arg1 >>> 0] = takeObject(arg2); +}; + +module.exports.__wbg_isArray_4c24b343cb13cfb1 = function(arg0) { + const ret = Array.isArray(getObject(arg0)); + return ret; +}; + +module.exports.__wbg_push_ca1c26067ef907ac = function(arg0, arg1) { + const ret = getObject(arg0).push(getObject(arg1)); + return ret; +}; + +module.exports.__wbg_instanceof_ArrayBuffer_39ac22089b74fddb = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof ArrayBuffer; + } catch { + result = false; + } + const ret = result; + return ret; +}; + +module.exports.__wbg_values_e80af618f92c8649 = function(arg0) { + const ret = getObject(arg0).values(); + return addHeapObject(ret); +}; + +module.exports.__wbg_set_bedc3d02d0f05eb0 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).set(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); +}; + +module.exports.__wbg_isSafeInteger_bb8e18dd21c97288 = function(arg0) { + const ret = Number.isSafeInteger(getObject(arg0)); + return ret; +}; + +module.exports.__wbg_buffer_085ec1f694018c4f = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); +}; + +module.exports.__wbg_new_8125e318e6245eed = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); +}; + +module.exports.__wbg_set_5cf90238115182c3 = function(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); +}; + +module.exports.__wbg_length_72e2208bbc0efc61 = function(arg0) { + const ret = getObject(arg0).length; + return ret; +}; + +module.exports.__wbg_instanceof_Uint8Array_d8d9cb2b8e8ac1d4 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Uint8Array; + } catch { + result = false; + } + const ret = result; + return ret; +}; + +module.exports.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; +}; + +module.exports.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + +module.exports.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); +}; + +const path = require('path').join(__dirname, 'bincode_js_bg.wasm'); +const bytes = require('fs').readFileSync(path); + +const wasmModule = new WebAssembly.Module(bytes); +const wasmInstance = new WebAssembly.Instance(wasmModule, imports); +wasm = wasmInstance.exports; +module.exports.__wasm = wasm; + diff --git a/worker/tests/wasm/bincode_js.d.ts b/worker/tests/wasm/bincode_js.d.ts new file mode 100644 index 0000000..69452b2 --- /dev/null +++ b/worker/tests/wasm/bincode_js.d.ts @@ -0,0 +1,225 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +* @param {any} val +* @returns {any} +*/ +export function bincode_js_deserialize(val: any): any; +/** +* @param {any} val +* @returns {any} +*/ +export function borsh_bpf_js_deserialize(val: any): any; +/** +* Initialize Javascript logging and panic handler +*/ +export function solana_program_init(): void; +/** +* A hash; the 32-byte output of a hashing algorithm. +* +* This struct is used most often in `solana-sdk` and related crates to contain +* a [SHA-256] hash, but may instead contain a [blake3] hash, as created by the +* [`blake3`] module (and used in [`Message::hash`]). +* +* [SHA-256]: https://en.wikipedia.org/wiki/SHA-2 +* [blake3]: https://github.com/BLAKE3-team/BLAKE3 +* [`blake3`]: crate::blake3 +* [`Message::hash`]: crate::message::Message::hash +*/ +export class Hash { + free(): void; +/** +* Create a new Hash object +* +* * `value` - optional hash as a base58 encoded string, `Uint8Array`, `[number]` +* @param {any} value +*/ + constructor(value: any); +/** +* Return the base58 string representation of the hash +* @returns {string} +*/ + toString(): string; +/** +* Checks if two `Hash`s are equal +* @param {Hash} other +* @returns {boolean} +*/ + equals(other: Hash): boolean; +/** +* Return the `Uint8Array` representation of the hash +* @returns {Uint8Array} +*/ + toBytes(): Uint8Array; +} +/** +* A directive for a single invocation of a Solana program. +* +* An instruction specifies which program it is calling, which accounts it may +* read or modify, and additional data that serves as input to the program. One +* or more instructions are included in transactions submitted by Solana +* clients. Instructions are also used to describe [cross-program +* invocations][cpi]. +* +* [cpi]: https://docs.solana.com/developing/programming-model/calling-between-programs +* +* During execution, a program will receive a list of account data as one of +* its arguments, in the same order as specified during `Instruction` +* construction. +* +* While Solana is agnostic to the format of the instruction data, it has +* built-in support for serialization via [`borsh`] and [`bincode`]. +* +* [`borsh`]: https://docs.rs/borsh/latest/borsh/ +* [`bincode`]: https://docs.rs/bincode/latest/bincode/ +* +* # Specifying account metadata +* +* When constructing an [`Instruction`], a list of all accounts that may be +* read or written during the execution of that instruction must be supplied as +* [`AccountMeta`] values. +* +* Any account whose data may be mutated by the program during execution must +* be specified as writable. During execution, writing to an account that was +* not specified as writable will cause the transaction to fail. Writing to an +* account that is not owned by the program will cause the transaction to fail. +* +* Any account whose lamport balance may be mutated by the program during +* execution must be specified as writable. During execution, mutating the +* lamports of an account that was not specified as writable will cause the +* transaction to fail. While _subtracting_ lamports from an account not owned +* by the program will cause the transaction to fail, _adding_ lamports to any +* account is allowed, as long is it is mutable. +* +* Accounts that are not read or written by the program may still be specified +* in an `Instruction`'s account list. These will affect scheduling of program +* execution by the runtime, but will otherwise be ignored. +* +* When building a transaction, the Solana runtime coalesces all accounts used +* by all instructions in that transaction, along with accounts and permissions +* required by the runtime, into a single account list. Some accounts and +* account permissions required by the runtime to process a transaction are +* _not_ required to be included in an `Instruction`s account list. These +* include: +* +* - The program ID — it is a separate field of `Instruction` +* - The transaction's fee-paying account — it is added during [`Message`] +* construction. A program may still require the fee payer as part of the +* account list if it directly references it. +* +* [`Message`]: crate::message::Message +* +* Programs may require signatures from some accounts, in which case they +* should be specified as signers during `Instruction` construction. The +* program must still validate during execution that the account is a signer. +*/ +export class Instruction { + free(): void; +} +/** +*/ +export class Instructions { + free(): void; +/** +*/ + constructor(); +/** +* @param {Instruction} instruction +*/ + push(instruction: Instruction): void; +} +/** +* A Solana transaction message (legacy). +* +* See the [`message`] module documentation for further description. +* +* [`message`]: crate::message +* +* Some constructors accept an optional `payer`, the account responsible for +* paying the cost of executing a transaction. In most cases, callers should +* specify the payer explicitly in these constructors. In some cases though, +* the caller is not _required_ to specify the payer, but is still allowed to: +* in the `Message` structure, the first account is always the fee-payer, so if +* the caller has knowledge that the first account of the constructed +* transaction's `Message` is both a signer and the expected fee-payer, then +* redundantly specifying the fee-payer is not strictly required. +*/ +export class Message { + free(): void; +/** +* The id of a recent ledger entry. +*/ + recent_blockhash: Hash; +} +/** +* The address of a [Solana account][acc]. +* +* Some account addresses are [ed25519] public keys, with corresponding secret +* keys that are managed off-chain. Often, though, account addresses do not +* have corresponding secret keys — as with [_program derived +* addresses_][pdas] — or the secret key is not relevant to the operation +* of a program, and may have even been disposed of. As running Solana programs +* can not safely create or manage secret keys, the full [`Keypair`] is not +* defined in `solana-program` but in `solana-sdk`. +* +* [acc]: https://docs.solana.com/developing/programming-model/accounts +* [ed25519]: https://ed25519.cr.yp.to/ +* [pdas]: https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses +* [`Keypair`]: https://docs.rs/solana-sdk/latest/solana_sdk/signer/keypair/struct.Keypair.html +*/ +export class Pubkey { + free(): void; +/** +* Create a new Pubkey object +* +* * `value` - optional public key as a base58 encoded string, `Uint8Array`, `[number]` +* @param {any} value +*/ + constructor(value: any); +/** +* Return the base58 string representation of the public key +* @returns {string} +*/ + toString(): string; +/** +* Check if a `Pubkey` is on the ed25519 curve. +* @returns {boolean} +*/ + isOnCurve(): boolean; +/** +* Checks if two `Pubkey`s are equal +* @param {Pubkey} other +* @returns {boolean} +*/ + equals(other: Pubkey): boolean; +/** +* Return the `Uint8Array` representation of the public key +* @returns {Uint8Array} +*/ + toBytes(): Uint8Array; +/** +* Derive a Pubkey from another Pubkey, string seed, and a program id +* @param {Pubkey} base +* @param {string} seed +* @param {Pubkey} owner +* @returns {Pubkey} +*/ + static createWithSeed(base: Pubkey, seed: string, owner: Pubkey): Pubkey; +/** +* Derive a program address from seeds and a program id +* @param {any[]} seeds +* @param {Pubkey} program_id +* @returns {Pubkey} +*/ + static createProgramAddress(seeds: any[], program_id: Pubkey): Pubkey; +/** +* Find a valid program address +* +* Returns: +* * `[PubKey, number]` - the program address and bump seed +* @param {any[]} seeds +* @param {Pubkey} program_id +* @returns {any} +*/ + static findProgramAddress(seeds: any[], program_id: Pubkey): any; +} diff --git a/worker/tests/wasm/bincode_js_bg.wasm b/worker/tests/wasm/bincode_js_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..2ef642ddd393c412151df55f4f9175ac952b6d39 GIT binary patch literal 192134 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