fetch-hackkit 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fetch_hackkit/__init__.py +1 -1
- fetch_hackkit/cli.py +38 -1
- fetch_hackkit/scaffold.py +122 -0
- fetch_hackkit/templates/agents_reference.md +237 -0
- fetch_hackkit/templates/project/AGENTS.md.tmpl +33 -0
- fetch_hackkit/templates/project/Dockerfile.tmpl +13 -0
- fetch_hackkit/templates/project/README.md.tmpl +29 -0
- fetch_hackkit/templates/project/agent.py.tmpl +102 -0
- fetch_hackkit/templates/project/cursor_mcp.json.tmpl +10 -0
- fetch_hackkit/templates/project/dockerignore.tmpl +7 -0
- fetch_hackkit/templates/project/env.example.tmpl +17 -0
- fetch_hackkit/templates/project/gitignore.tmpl +9 -0
- fetch_hackkit/templates/project/mcp.json.tmpl +11 -0
- fetch_hackkit/templates/project/opencode.json.tmpl +83 -0
- fetch_hackkit/templates/project/pyproject.toml.tmpl +21 -0
- fetch_hackkit/templates/project/python-version.tmpl +1 -0
- fetch_hackkit/templates/project/tests/test_agent.py.tmpl +23 -0
- {fetch_hackkit-0.1.0.dist-info → fetch_hackkit-0.2.0.dist-info}/METADATA +10 -1
- fetch_hackkit-0.2.0.dist-info/RECORD +31 -0
- fetch_hackkit-0.1.0.dist-info/RECORD +0 -16
- {fetch_hackkit-0.1.0.dist-info → fetch_hackkit-0.2.0.dist-info}/WHEEL +0 -0
- {fetch_hackkit-0.1.0.dist-info → fetch_hackkit-0.2.0.dist-info}/entry_points.txt +0 -0
- {fetch_hackkit-0.1.0.dist-info → fetch_hackkit-0.2.0.dist-info}/licenses/LICENSE +0 -0
fetch_hackkit/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from .asi1 import ASI1, ASI1Error, Tool, history, remember, tool
|
|
|
9
9
|
from .cards import Card, CardError, parse_selection
|
|
10
10
|
from .register import connect_mailbox, handle_available, resolve, validate_handle
|
|
11
11
|
|
|
12
|
-
__version__ = "0.
|
|
12
|
+
__version__ = "0.2.0"
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
15
|
"ASI1",
|
fetch_hackkit/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""``hackkit`` command line: seed, whoami, handle, doctor."""
|
|
1
|
+
"""``hackkit`` command line: new, seed, whoami, handle, doctor."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -51,6 +51,37 @@ def _handle(args: argparse.Namespace) -> int:
|
|
|
51
51
|
return 1
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _new(args: argparse.Namespace) -> int:
|
|
55
|
+
from .scaffold import create_project
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
project = create_project(
|
|
59
|
+
args.name,
|
|
60
|
+
directory=Path(args.dir) if args.dir else None,
|
|
61
|
+
handle=args.handle,
|
|
62
|
+
check_handle=not args.no_check,
|
|
63
|
+
)
|
|
64
|
+
except (FileExistsError, ValueError) as exc:
|
|
65
|
+
print(f"❌ {exc}")
|
|
66
|
+
return 1
|
|
67
|
+
try:
|
|
68
|
+
where = project.path.relative_to(Path.cwd())
|
|
69
|
+
except ValueError:
|
|
70
|
+
where = project.path
|
|
71
|
+
print(f"✅ Created {project.name} in {where}/")
|
|
72
|
+
print(f" handle: @{project.handle} ({project.handle_note})")
|
|
73
|
+
print(f" address: {project.address}")
|
|
74
|
+
print(" identity: a fresh seed is in .env (never commit it)\n")
|
|
75
|
+
print("Next:")
|
|
76
|
+
print(f" cd {where}")
|
|
77
|
+
print(" # add ASI_ONE_API_KEY and AGENTVERSE_API_KEY to .env")
|
|
78
|
+
print(" uv sync")
|
|
79
|
+
print(" uv run python agent.py")
|
|
80
|
+
print(f" # then on https://asi1.ai say: @{project.handle} hi")
|
|
81
|
+
print("\nCoding agent: run `opencode` here (ASI:One + Agentverse MCP), or Claude Code/Cursor.")
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
54
85
|
def _doctor(args: argparse.Namespace) -> int:
|
|
55
86
|
from .doctor import run
|
|
56
87
|
|
|
@@ -69,6 +100,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
69
100
|
load_dotenv(find_dotenv(usecwd=True))
|
|
70
101
|
parser = argparse.ArgumentParser(prog="hackkit", description="Fetch.ai hackathon helpers")
|
|
71
102
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
103
|
+
p = sub.add_parser("new", help="create a ready-to-run agent project")
|
|
104
|
+
p.add_argument("name", help='agent name, e.g. "Trip Planner"')
|
|
105
|
+
p.add_argument("--dir", help="where to create it (default: ./<name-slug>)")
|
|
106
|
+
p.add_argument("--handle", help="Agentverse @handle (default: from the name)")
|
|
107
|
+
p.add_argument("--no-check", action="store_true", help="don't check handle availability")
|
|
108
|
+
p.set_defaults(fn=_new)
|
|
72
109
|
sub.add_parser("seed", help="generate a secure seed phrase + show its address").set_defaults(
|
|
73
110
|
fn=_seed
|
|
74
111
|
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""``hackkit new``: generate a minimal, ready-to-run agent project.
|
|
2
|
+
|
|
3
|
+
The project gets an agent with a menu card and an ASI:One tool, a README in
|
|
4
|
+
Agentverse profile shape, a fresh seed in ``.env``, tests, a Dockerfile, and
|
|
5
|
+
AGENTS.md plus opencode/Claude Code/Cursor configs for coding agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import secrets
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from importlib.resources import files
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .register import HANDLE_MAX_LEN, resolve, validate_handle
|
|
18
|
+
|
|
19
|
+
# template file (under templates/project) -> path in the new project
|
|
20
|
+
FILES = {
|
|
21
|
+
"agent.py.tmpl": "agent.py",
|
|
22
|
+
"README.md.tmpl": "README.md",
|
|
23
|
+
"pyproject.toml.tmpl": "pyproject.toml",
|
|
24
|
+
"python-version.tmpl": ".python-version",
|
|
25
|
+
"env.example.tmpl": ".env.example",
|
|
26
|
+
"gitignore.tmpl": ".gitignore",
|
|
27
|
+
"dockerignore.tmpl": ".dockerignore",
|
|
28
|
+
"Dockerfile.tmpl": "Dockerfile",
|
|
29
|
+
"tests/test_agent.py.tmpl": "tests/test_agent.py",
|
|
30
|
+
"opencode.json.tmpl": "opencode.json",
|
|
31
|
+
"mcp.json.tmpl": ".mcp.json",
|
|
32
|
+
"cursor_mcp.json.tmpl": ".cursor/mcp.json",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Project:
|
|
38
|
+
path: Path
|
|
39
|
+
name: str
|
|
40
|
+
handle: str
|
|
41
|
+
address: str
|
|
42
|
+
handle_note: str = ""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def slugify(name: str) -> str:
|
|
46
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
47
|
+
return slug or "my-agent"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _template(rel: str) -> str:
|
|
51
|
+
return (files("fetch_hackkit") / "templates" / rel).read_text(encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _choose_handle(wanted: str, check: bool) -> tuple[str, str]:
|
|
55
|
+
"""Return (handle, note). Picks a free variant if ``wanted`` is taken."""
|
|
56
|
+
handle = validate_handle(wanted[:HANDLE_MAX_LEN].strip("-") or "my-agent")
|
|
57
|
+
if not check:
|
|
58
|
+
return handle, "not checked (--no-check)"
|
|
59
|
+
try:
|
|
60
|
+
if resolve(handle) is None:
|
|
61
|
+
return handle, "available"
|
|
62
|
+
for _ in range(5):
|
|
63
|
+
candidate = validate_handle(f"{handle[: HANDLE_MAX_LEN - 5]}-{secrets.token_hex(2)}")
|
|
64
|
+
if resolve(candidate) is None:
|
|
65
|
+
return candidate, f"@{handle} was taken"
|
|
66
|
+
except Exception as exc: # offline etc.: keep the wanted handle
|
|
67
|
+
return handle, f"could not check availability ({type(exc).__name__})"
|
|
68
|
+
return handle, f"@{handle} is taken; pick another with `hackkit handle`"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def create_project(
|
|
72
|
+
name: str,
|
|
73
|
+
*,
|
|
74
|
+
directory: Path | None = None,
|
|
75
|
+
handle: str | None = None,
|
|
76
|
+
check_handle: bool = True,
|
|
77
|
+
) -> Project:
|
|
78
|
+
"""Write a new agent project and return where it is and who it is."""
|
|
79
|
+
from . import __version__
|
|
80
|
+
from .doctor import identity_from_seed
|
|
81
|
+
|
|
82
|
+
path = (directory or Path.cwd() / slugify(name)).resolve()
|
|
83
|
+
if path.exists() and any(path.iterdir()):
|
|
84
|
+
raise FileExistsError(f"{path} already exists and is not empty")
|
|
85
|
+
|
|
86
|
+
chosen, note = _choose_handle(
|
|
87
|
+
handle.removeprefix("@") if handle else slugify(name), check_handle
|
|
88
|
+
)
|
|
89
|
+
seed = secrets.token_urlsafe(32)
|
|
90
|
+
address, _wallet = identity_from_seed(seed)
|
|
91
|
+
values = {
|
|
92
|
+
"__AGENT_NAME__": name,
|
|
93
|
+
"__AGENT_HANDLE__": chosen,
|
|
94
|
+
"__AGENT_ADDRESS__": address,
|
|
95
|
+
"__PROJECT_SLUG__": slugify(name),
|
|
96
|
+
"__HACKKIT_VERSION__": __version__,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def render(text: str) -> str:
|
|
100
|
+
for key, value in values.items():
|
|
101
|
+
text = text.replace(key, value)
|
|
102
|
+
return text
|
|
103
|
+
|
|
104
|
+
for src, dest in FILES.items():
|
|
105
|
+
target = path / dest
|
|
106
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
target.write_text(render(_template(f"project/{src}")), encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
agents_md = render(_template("project/AGENTS.md.tmpl")) + _template("agents_reference.md")
|
|
110
|
+
(path / "AGENTS.md").write_text(agents_md, encoding="utf-8")
|
|
111
|
+
try:
|
|
112
|
+
os.symlink("AGENTS.md", path / "CLAUDE.md")
|
|
113
|
+
except OSError: # e.g. Windows without symlink rights
|
|
114
|
+
(path / "CLAUDE.md").write_text(agents_md, encoding="utf-8")
|
|
115
|
+
|
|
116
|
+
env = render(_template("project/env.example.tmpl")).replace(
|
|
117
|
+
"AGENT_SEED_PHRASE=\n", f"AGENT_SEED_PHRASE={seed}\n", 1
|
|
118
|
+
)
|
|
119
|
+
env_path = path / ".env"
|
|
120
|
+
env_path.write_text(env, encoding="utf-8")
|
|
121
|
+
env_path.chmod(0o600)
|
|
122
|
+
return Project(path=path, name=name, handle=chosen, address=address, handle_note=note)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
## Golden rules
|
|
2
|
+
|
|
3
|
+
1. **Use `HackAgent`, not a bare `uagents.Agent`.** It includes the chat protocol, publishes its manifest, sends acknowledgements, and turns handler exceptions into a friendly reply.
|
|
4
|
+
2. **Handlers return replies; they don't call `ctx.send` for chat.** Return a `str`, a `cards.Card`, a `ChatMessage`, or a list of these.
|
|
5
|
+
3. **Never block the event loop.** Use `httpx.AsyncClient` / `async` SDKs. Wrap blocking calls in `await asyncio.to_thread(fn, ...)`.
|
|
6
|
+
4. **Secrets live in `.env`** (gitignored): `AGENT_SEED_PHRASE`, `ASI_ONE_API_KEY`, `AGENTVERSE_API_KEY`.
|
|
7
|
+
- Never hard-code them. Never print the seed.
|
|
8
|
+
- The seed *is* the agent: the same seed always gives the same address and wallet.
|
|
9
|
+
5. **Keep per-conversation state in `ctx.storage`, keyed by `msg.session`.** ASI:One sends your agent one message at a time.
|
|
10
|
+
6. **Every card has a narration.** It's the fallback text if a card fails to render.
|
|
11
|
+
7. **Run `uv run pytest -q` and `uv run ruff check .` after changes.**
|
|
12
|
+
|
|
13
|
+
## The agent pattern
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from fetch_hackkit import HackAgent, cards
|
|
17
|
+
|
|
18
|
+
agent = HackAgent(
|
|
19
|
+
name="Trip Planner", # display name on Agentverse
|
|
20
|
+
handle="trip-planner", # @handle: unique, <= 40 chars
|
|
21
|
+
description="Plans weekend trips with live prices.", # short description
|
|
22
|
+
readme_path="README.md", # long description (ASI:One routing reads it)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@agent.on_text # free text (leading @mentions already stripped)
|
|
26
|
+
async def on_text(ctx, msg): # msg: Inbound(sender, text, session, selection, raw)
|
|
27
|
+
return cards.carousel("Pick a city", [
|
|
28
|
+
cards.item("sf", "San Francisco", action="pick_city"),
|
|
29
|
+
cards.item("la", "Los Angeles", action="pick_city"),
|
|
30
|
+
], narration="Where to?")
|
|
31
|
+
|
|
32
|
+
@agent.on_action("pick_city") # card taps, routed by selection["action"]
|
|
33
|
+
async def pick_city(ctx, msg, sel): # sel == {"action": "pick_city", "id": "sf"}
|
|
34
|
+
return f"Planning a trip to {sel['id']}..."
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
agent.run()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`HackAgent` subclasses `uagents.Agent`, so `@agent.on_interval(period=60)`, `@agent.on_event("startup")`, `agent.include(protocol)` and `ctx.storage` all work.
|
|
41
|
+
|
|
42
|
+
## Interactive cards (ASI:One UI)
|
|
43
|
+
|
|
44
|
+
A card is a chat message with narration text plus a `MetadataContent` declaration. ASI:One renders it as UI. When the user taps a button, the button's **selection** comes back to your agent as a message.
|
|
45
|
+
|
|
46
|
+
### Pick the right card
|
|
47
|
+
|
|
48
|
+
| Card | Use for | Builder |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `carousel` | Menus, search results, options (one button per item) | `cards.carousel(title, [cards.item(id, title, action=...)])` |
|
|
51
|
+
| `detail` | One thing in depth: image, key/value rows, radio choice, several buttons | `cards.detail(title, rows={...}, ctas=[cards.cta(...)], sub_options=cards.radio(...))` |
|
|
52
|
+
| `form` | Collect input (`text`, `number`, `email`, `select`, `checkbox`) | `cards.form(title, [cards.field(...)], submit=cards.cta(...))` |
|
|
53
|
+
| `review` | Confirm before anything irreversible (payment, booking, sending) | `cards.review(title, rows=..., approve=cards.cta(...), reject=cards.cta(...))` |
|
|
54
|
+
| `custom` | Anything else, built from primitives | `cards.custom(ui.section(title, [ui.text(...), ui.button(...)]))` |
|
|
55
|
+
| read-only | Show a result that needs no input | `cards.info(title, rows=...)` |
|
|
56
|
+
|
|
57
|
+
Custom primitives live in `fetch_hackkit.cards.ui`:
|
|
58
|
+
- **Layout:** `section`, `group(direction="row"|"column")`, `divider`.
|
|
59
|
+
- **Content:** `text(style=)`, `heading(level=)`, `image`, `badge(variant="info"|"success"|"warning")`.
|
|
60
|
+
- **Interactive:** `button`, `input`, `list_items` + `list_item`, `choice_grid` (image picker).
|
|
61
|
+
|
|
62
|
+
### Selections: what comes back
|
|
63
|
+
|
|
64
|
+
Every button built with `cards.cta(label, action, **data)`, `cards.item(..., action=...)` or `ui.button(label, action, **data)` sends `{"action": action, **data}`. The values that come back depend on the card:
|
|
65
|
+
|
|
66
|
+
| Card | What arrives |
|
|
67
|
+
|---|---|
|
|
68
|
+
| carousel item | `{"action", "id", ...data}` |
|
|
69
|
+
| detail | the button's selection, plus the radio value under `sub_options.name` |
|
|
70
|
+
| form | the submit button's selection, plus **every field value** keyed by field name |
|
|
71
|
+
| review | the approve or the reject selection |
|
|
72
|
+
| custom | the button's selection, plus every `input` / `choice_grid` value keyed by `name` |
|
|
73
|
+
|
|
74
|
+
It arrives in one of two formats, and `HackAgent` handles both:
|
|
75
|
+
- **Direct @mention:** JSON. `sel` is the full dict.
|
|
76
|
+
- **Routed by the ASI:One planner:** a prose sentence containing the selection's identifiers. `HackAgent` finds the action word and gives you `{"action": ..., "prose": "<sentence>"}`. Field values may only be inside `msg.text`.
|
|
77
|
+
|
|
78
|
+
Design rules that follow:
|
|
79
|
+
- **Make `action` names distinctive tokens** (`book_hotel`, not `ok`) so they can be found in prose.
|
|
80
|
+
- **For important values, check `"prose" in sel`** and either recover them from `msg.text` or ask again.
|
|
81
|
+
- **Unique ids in `data`** (e.g. `id="trip_42"`) also appear in the prose.
|
|
82
|
+
- **One active card:** pass `card_id=` to `Card.to_message(card_id=...)` (or `cards.card_message`), and ignore taps whose `sel.get("card_id")` isn't the latest one.
|
|
83
|
+
|
|
84
|
+
### Limits (enforced by `cards.validate`, which raises `CardError`)
|
|
85
|
+
- Payload of 64 KB or less (the kit aims for under 60 KB).
|
|
86
|
+
- Custom trees at most 8 levels deep.
|
|
87
|
+
- `select` fields need `options`.
|
|
88
|
+
- Only these card kinds exist; there is no standalone "buttons" kind. Use `custom` with `ui.button`.
|
|
89
|
+
|
|
90
|
+
If ASI:One rejects a card, the user sees only the narration. That's why every card needs one.
|
|
91
|
+
|
|
92
|
+
## ASI:One LLM (`fetch_hackkit.asi1`)
|
|
93
|
+
|
|
94
|
+
The API is OpenAI-compatible (`https://api.asi1.ai/v1`). Models: `asi1` (default), `asi1-ultra`, `asi1-mini`. Set `ASI_ONE_MODEL` to switch.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from fetch_hackkit import ASI1, history, remember
|
|
98
|
+
|
|
99
|
+
llm = ASI1() # raises ASI1Error if no key
|
|
100
|
+
text = await llm.ask("Summarise this", system="Be brief")
|
|
101
|
+
plan = await llm.structured("Plan a trip to SF", TripPlan) # Pydantic model, validated
|
|
102
|
+
answer = await llm.run_tools(question, tools=[get_weather, search_flights])
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **Tools are plain functions,** sync or async, with type hints and a docstring. Google-style `Args:` lines become parameter descriptions.
|
|
106
|
+
- **Failures don't crash the agent:** a tool exception goes back to the model as `{"error": ...}`.
|
|
107
|
+
- **Use `run_tools`** to make the agent *act*, not just chat. Judges penalise "a thin wrapper around an API".
|
|
108
|
+
- **Multi-turn memory:** `history(ctx, msg.session)` and `remember(ctx, msg.session, role, text)`.
|
|
109
|
+
|
|
110
|
+
## Payments (`@agent.paid`)
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@agent.paid("premium_report", amount="0.5", description="Premium market report")
|
|
114
|
+
async def premium(ctx, msg, sel): # runs only after payment is verified on-chain
|
|
115
|
+
return await build_report(sel)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
1. A tap with `action="premium_report"`, or `await agent.charge(ctx, msg, "premium_report")`, sends `RequestPayment` (FET, `fet_direct`, the agent's wallet as recipient).
|
|
119
|
+
2. The user approves in ASI:One. The agent receives `CommitPayment(transaction_id)`.
|
|
120
|
+
3. The kit verifies the transfer on-chain (recipient, amount, buyer) with cosmpy, retrying while the transaction indexes. It then sends `CompletePayment` and runs your handler. Otherwise it sends `CancelPayment(reason)`.
|
|
121
|
+
4. Replayed commits are acknowledged but never delivered twice.
|
|
122
|
+
|
|
123
|
+
Notes:
|
|
124
|
+
- **Network:** stable testnet by default (`FET_NETWORK=mainnet` for real FET). Test FET is free from the Fetch.ai testnet faucet.
|
|
125
|
+
- **Show the price first:** put a `review` card before an expensive paid action.
|
|
126
|
+
- **Never deliver paid work before the handler runs.**
|
|
127
|
+
|
|
128
|
+
## MCP
|
|
129
|
+
|
|
130
|
+
**Use an MCP server's tools inside your agent:**
|
|
131
|
+
```python
|
|
132
|
+
from fetch_hackkit.mcp import mcp_tools, call_mcp_tool
|
|
133
|
+
tools = await mcp_tools("https://mcp.deepwiki.com/mcp", include={"ask_wiki_question"})
|
|
134
|
+
answer = await ASI1().run_tools("How does X work in fetchai/uAgents?", tools=tools)
|
|
135
|
+
```
|
|
136
|
+
Pass `headers={"Authorization": "Bearer ..."}` for authenticated servers.
|
|
137
|
+
|
|
138
|
+
**Publish an MCP server as an Agentverse agent:** use `uagents-adapter[mcp]` (`MCPServerAdapter`). It's not part of this kit.
|
|
139
|
+
|
|
140
|
+
## Multi-agent
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from fetch_hackkit.peers import ask_agent, PeerError
|
|
144
|
+
reply = await ask_agent(ctx, "@some-handle", "question", timeout=60) # or an agent1... address
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`ask_agent` works with any chat-protocol agent on Agentverse. For your own specialist, run a second `HackAgent` with its own seed and port, then call it by address or `@handle`.
|
|
148
|
+
|
|
149
|
+
## Registration and the Agentverse profile
|
|
150
|
+
|
|
151
|
+
### How the agent gets onto Agentverse
|
|
152
|
+
|
|
153
|
+
`HackAgent` uses a **mailbox** by default, so it runs on your laptop but is reachable through Agentverse.
|
|
154
|
+
- **With `AGENTVERSE_API_KEY` set:** on startup it proves ownership of its address and publishes its profile. No clicks needed.
|
|
155
|
+
- **Without the key:** open the inspector link the agent logs and click **Connect → Mailbox**.
|
|
156
|
+
- **Agent at a public URL** (Render, Fly, a VM): call `fetch_hackkit.register.register_endpoint_agent(agent, "https://.../submit")`.
|
|
157
|
+
|
|
158
|
+
### Where each profile field comes from
|
|
159
|
+
|
|
160
|
+
| Profile field | Set it in code | Change it later via the Agentverse MCP |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| Name | `HackAgent(name=...)` | `update_user_agent_mailbox` (mailbox/local) / `update_user_agent` (hosted): `name` |
|
|
163
|
+
| **@handle** | `HackAgent(handle=...)` or `AGENT_HANDLE` in `.env` | **Not possible via MCP.** Change it in code and restart. |
|
|
164
|
+
| Short description | `HackAgent(description=...)` | same tools: `short_description` |
|
|
165
|
+
| README | `HackAgent(readme_path=...)` | same tools: `readme` |
|
|
166
|
+
| Avatar | `HackAgent(avatar_url=...)` | same tools: `avatar_url` |
|
|
167
|
+
| Protocols | automatic: chat, plus payment if `@agent.paid` is used | read-only |
|
|
168
|
+
|
|
169
|
+
Handle rules:
|
|
170
|
+
- 1–40 characters: letters, digits, `-`, `_` and `.`.
|
|
171
|
+
- Unique across all of Agentverse.
|
|
172
|
+
- Check availability first with `uv run hackkit handle <name>`. A taken handle fails registration with HTTP 409.
|
|
173
|
+
- Agentverse only applies `handle` when it *updates* an existing agent, not when it first creates one. `HackAgent`'s auto-connect checks and re-sends for you. If you connect another way (e.g. the inspector button), restart the agent once or run `hackkit doctor` to confirm the handle stuck.
|
|
174
|
+
|
|
175
|
+
### Agentverse MCP (configured in `opencode.json`, `.mcp.json`, `.cursor/mcp.json`)
|
|
176
|
+
|
|
177
|
+
- **Server:** `https://mcp.agentverse.ai/mcp` (streamable HTTP, ~126 tools), enabled by default.
|
|
178
|
+
- **Lite server** (`https://mcp-lite.agentverse.ai/mcp`): fewer tools, so it's friendlier to smaller models. It's included but disabled in `opencode.json` because it wasn't responding when this starter was tested (2026-10-22). Enable it if it's back up.
|
|
179
|
+
- **Auth:** an `Authorization: Bearer $AGENTVERSE_API_KEY` header covers public lookups (`get_agent`, `get_agent_search_api_internal`, `search_agents`). Tools that act on *your* agents (`get_user_agent_*`, `update_user_agent_*`, `create_user_agent`, …) also need the key passed as their `api_token` argument. Read it from the environment; never paste it into chat.
|
|
180
|
+
|
|
181
|
+
Useful workflows:
|
|
182
|
+
- **Inspect your agent:**
|
|
183
|
+
- `get_user_agent_details` / `get_agent` with your address;
|
|
184
|
+
- `agent_readiness_probe_mailbox` (mailbox) / `agent_readiness_probe` (hosted).
|
|
185
|
+
- **Edit your profile:** `update_user_agent_mailbox(address, name, short_description, readme)`.
|
|
186
|
+
- **Hosted agent** (runs on Agentverse, no laptop): `create_user_agent` → `update_user_agent_code` → `start_specific_user_agent` → `get_latest_logs_for_user_agent`. See *Keeping your agent online* for the catch.
|
|
187
|
+
- **Discoverability:**
|
|
188
|
+
- `search_agents` with the words a user would type, to see where you rank;
|
|
189
|
+
- `start_agent_seo_eval` → `get_agent_seo_eval_results` to see how well ASI:One can match you.
|
|
190
|
+
|
|
191
|
+
### Getting discovered by ASI:One
|
|
192
|
+
|
|
193
|
+
ASI:One routes users by your **name, description, README and handle**:
|
|
194
|
+
- Write the README for routing: what the agent does, concrete example prompts, and the domains it covers.
|
|
195
|
+
- Keep the agent running (status `active`). A stopped mailbox agent isn't routed to.
|
|
196
|
+
|
|
197
|
+
## Keeping your agent online
|
|
198
|
+
|
|
199
|
+
A mailbox agent only answers while its process is running. Judges will message yours, so plan for it.
|
|
200
|
+
|
|
201
|
+
- **It needs only outbound internet:** no public URL, no open ports, no domain.
|
|
202
|
+
- **While hacking:** run it on your laptop. On macOS, keep the laptop awake with `caffeinate -i uv run python agent.py`.
|
|
203
|
+
- **For judging and after:** run the `Dockerfile` on any always-on host as a *background worker* (no HTTP service), with your `.env` values as environment variables:
|
|
204
|
+
```bash
|
|
205
|
+
docker build -t my-agent .
|
|
206
|
+
docker run -d --restart unless-stopped --env-file .env my-agent
|
|
207
|
+
```
|
|
208
|
+
Any container host works: a small VM, Render (Background Worker), Railway, Fly.io Machines.
|
|
209
|
+
- **Run exactly one copy per seed.** Two processes with the same seed poll the same mailbox and split your messages.
|
|
210
|
+
- **`ctx.storage` is a local JSON file.** On hosts with throwaway disks it resets on redeploy. That's fine for a demo; use a database for anything you must keep.
|
|
211
|
+
- **Agentverse-hosted agents** (`create_user_agent` via MCP) run in Agentverse's own sandbox with a fixed set of packages, so they can't install `fetch-hackkit`. Prefer the mailbox + Docker route.
|
|
212
|
+
|
|
213
|
+
## Submission checklist (Fetch.ai prize)
|
|
214
|
+
|
|
215
|
+
- [ ] Registered on Agentverse, status active, chat protocol published. `hackkit doctor` checks all of this.
|
|
216
|
+
- [ ] The agent stays online through judging (see *Keeping your agent online*).
|
|
217
|
+
- [ ] Works end-to-end inside ASI:One chat, with no custom frontend.
|
|
218
|
+
- [ ] Real tool use, MCP, or multi-agent orchestration, not just one LLM call.
|
|
219
|
+
- [ ] (Bonus) Interactive cards, the Payment Protocol, and error handling and recovery.
|
|
220
|
+
- [ ] Public GitHub repo README with **agent name + address** and both badges:
|
|
221
|
+
``
|
|
222
|
+
``
|
|
223
|
+
- [ ] A 3–5 minute demo video.
|
|
224
|
+
|
|
225
|
+
## Troubleshooting
|
|
226
|
+
|
|
227
|
+
| Symptom | Fix |
|
|
228
|
+
|---|---|
|
|
229
|
+
| `No seed phrase` | `uv run hackkit seed`, then put the phrase in `.env` |
|
|
230
|
+
| Agent not found in ASI:One | Is it running? Run `hackkit doctor`. Make the README specific, then try `@your-handle` directly. |
|
|
231
|
+
| `HTTP 409` on connect | Handle (or address) taken by another account. Pick a new handle. |
|
|
232
|
+
| `HTTP 401` on connect | Bad or expired `AGENTVERSE_API_KEY` |
|
|
233
|
+
| Card shows as plain text | The card is invalid. Look for `CardError` in the logs. |
|
|
234
|
+
| Button tap ignored | No `@agent.on_action` for that action, so it falls through to `on_text`. Check `msg.selection`. |
|
|
235
|
+
| Payment "not found" | The transaction is still indexing; the kit retries 3 times. On mainnet, check `FET_NETWORK`. |
|
|
236
|
+
| Log warns `not enough funds to register on Almanac contract` | Harmless. Agentverse (API) registration is what ASI:One uses; the on-chain contract registration is optional. |
|
|
237
|
+
| `There is no current event loop` (Python 3.14) | Use `HackAgent`, which creates the loop for you, or run on Python 3.13 |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guide for AI coding agents (opencode, Claude Code, Cursor, Codex, and others) working in this project. Humans: this is also the fastest complete reference.
|
|
4
|
+
|
|
5
|
+
You are helping a hackathon team build a **Fetch.ai agent** called **__AGENT_NAME__** (`@__AGENT_HANDLE__`). To win, the agent must:
|
|
6
|
+
- be registered on **Agentverse**;
|
|
7
|
+
- be discoverable and usable inside **ASI:One** chat (the Agent Chat Protocol);
|
|
8
|
+
- do real work: tools, APIs, MCP, or other agents;
|
|
9
|
+
- ideally use **interactive cards** and the **Payment Protocol**.
|
|
10
|
+
|
|
11
|
+
The `fetch-hackkit` package does the plumbing. Write agent code against it.
|
|
12
|
+
|
|
13
|
+
## Project map
|
|
14
|
+
|
|
15
|
+
| Path | What it is |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `agent.py` | The agent: `HackAgent`, its handlers, cards and tools. Start here. |
|
|
18
|
+
| `README.md` | Shown on the Agentverse profile; ASI:One reads it to route users here. Keep it specific. |
|
|
19
|
+
| `tests/` | Tests. At minimum, every card you send must pass `cards.validate`. |
|
|
20
|
+
| `.env` | Secrets and identity (gitignored). `.env.example` documents every variable. |
|
|
21
|
+
| `Dockerfile` | Runs the agent on an always-on host (see *Keeping your agent online*). |
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync # install (Python 3.11+)
|
|
27
|
+
uv run python agent.py # run the agent
|
|
28
|
+
uv run hackkit whoami # address, wallet, Agentverse profile URL
|
|
29
|
+
uv run hackkit handle my-agent # is @my-agent free on Agentverse?
|
|
30
|
+
uv run hackkit doctor # pre-submission checks
|
|
31
|
+
uv run pytest -q && uv run ruff check .
|
|
32
|
+
```
|
|
33
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Runs the agent as a background worker. It needs outbound internet only:
|
|
2
|
+
# docker build -t __PROJECT_SLUG__ .
|
|
3
|
+
# docker run -d --restart unless-stopped --env-file .env __PROJECT_SLUG__
|
|
4
|
+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
|
5
|
+
|
|
6
|
+
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy PYTHONUNBUFFERED=1
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
COPY pyproject.toml uv.lock* ./
|
|
10
|
+
RUN uv sync --no-dev --no-install-project
|
|
11
|
+
|
|
12
|
+
COPY . .
|
|
13
|
+
CMD ["uv", "run", "--no-dev", "python", "agent.py"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
# __AGENT_NAME__
|
|
5
|
+
|
|
6
|
+
TODO: what your agent does, for whom, and why it's useful, in 2-3 sentences. Agentverse shows this README on your agent's profile, and ASI:One reads it to decide when to send users to you.
|
|
7
|
+
|
|
8
|
+
**Handle:** `@__AGENT_HANDLE__`
|
|
9
|
+
|
|
10
|
+
**Agent address:** `__AGENT_ADDRESS__`
|
|
11
|
+
|
|
12
|
+
## What it can do
|
|
13
|
+
|
|
14
|
+
- TODO: capability 1
|
|
15
|
+
- TODO: capability 2
|
|
16
|
+
|
|
17
|
+
## Example prompts
|
|
18
|
+
|
|
19
|
+
- Hi
|
|
20
|
+
- What's the weather in Berkeley?
|
|
21
|
+
|
|
22
|
+
## Run it
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv sync
|
|
26
|
+
uv run python agent.py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Built with [fetch-hackkit](https://pypi.org/project/fetch-hackkit/), uAgents, the Agent Chat Protocol and ASI:One.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""__AGENT_NAME__: a Fetch.ai agent built with fetch-hackkit.
|
|
2
|
+
|
|
3
|
+
Run it: uv run python agent.py
|
|
4
|
+
Then chat with it on ASI:One (https://asi1.ai): @__AGENT_HANDLE__ hi
|
|
5
|
+
|
|
6
|
+
Start here: change the description, replace get_weather with your own tools,
|
|
7
|
+
and add cards for the steps of your workflow. AGENTS.md has every pattern.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from fetch_hackkit import ASI1, ASI1Error, HackAgent, cards, history, remember
|
|
16
|
+
|
|
17
|
+
agent = HackAgent(
|
|
18
|
+
name="__AGENT_NAME__",
|
|
19
|
+
handle=os.getenv("AGENT_HANDLE") or None,
|
|
20
|
+
# One sentence ASI:One users would search for. ASI:One routes by this + README.md.
|
|
21
|
+
description="TODO: describe what your agent does in one sentence.",
|
|
22
|
+
readme_path="README.md",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
SYSTEM = (
|
|
26
|
+
"You are __AGENT_NAME__, an agent on Fetch.ai. Be concise. "
|
|
27
|
+
"Use your tools for facts instead of guessing."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
GREETINGS = {"hi", "hello", "hey", "menu", "start", "help"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def get_weather(city: str) -> str:
|
|
34
|
+
"""Current weather for a city.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
city: City name, e.g. Berkeley.
|
|
38
|
+
"""
|
|
39
|
+
async with httpx.AsyncClient(timeout=15) as client:
|
|
40
|
+
resp = await client.get(f"https://wttr.in/{city}", params={"format": "3"})
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
return resp.text.strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def menu() -> cards.Card:
|
|
46
|
+
return cards.carousel(
|
|
47
|
+
"__AGENT_NAME__",
|
|
48
|
+
[
|
|
49
|
+
cards.item(
|
|
50
|
+
"ask",
|
|
51
|
+
"Ask a question",
|
|
52
|
+
action="ask_help",
|
|
53
|
+
subtitle="ASI:One answers, calling tools when needed",
|
|
54
|
+
),
|
|
55
|
+
cards.item(
|
|
56
|
+
"about", "What can you do?", action="about", subtitle="A quick tour of this agent"
|
|
57
|
+
),
|
|
58
|
+
],
|
|
59
|
+
narration="Hi! I'm **__AGENT_NAME__**. Pick an option or just ask me something.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@agent.on_text
|
|
64
|
+
async def on_text(ctx, msg):
|
|
65
|
+
"""Greetings open the menu; everything else goes to ASI:One with tools."""
|
|
66
|
+
if msg.text.strip().lower() in GREETINGS:
|
|
67
|
+
return menu()
|
|
68
|
+
try:
|
|
69
|
+
llm = ASI1()
|
|
70
|
+
except ASI1Error:
|
|
71
|
+
return "Add ASI_ONE_API_KEY to .env so I can answer questions."
|
|
72
|
+
past = history(ctx, msg.session)
|
|
73
|
+
reply = await llm.run_tools(
|
|
74
|
+
[*past, {"role": "user", "content": msg.text}],
|
|
75
|
+
tools=[get_weather],
|
|
76
|
+
system=SYSTEM,
|
|
77
|
+
)
|
|
78
|
+
remember(ctx, msg.session, "user", msg.text)
|
|
79
|
+
remember(ctx, msg.session, "assistant", reply)
|
|
80
|
+
return reply
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@agent.on_action("ask_help")
|
|
84
|
+
async def ask_help(ctx, msg, sel):
|
|
85
|
+
return "Ask me anything. Try: *What's the weather in Berkeley?*"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@agent.on_action("about")
|
|
89
|
+
async def about(ctx, msg, sel):
|
|
90
|
+
return cards.info(
|
|
91
|
+
"About __AGENT_NAME__",
|
|
92
|
+
rows={
|
|
93
|
+
"Talks via": "Agent Chat Protocol (ASI:One)",
|
|
94
|
+
"Brain": "ASI:One LLM with tool calling",
|
|
95
|
+
"Tools": "get_weather (replace with yours)",
|
|
96
|
+
},
|
|
97
|
+
narration="Here's what I'm built from.",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
agent.run()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Your agent's identity. `hackkit new` already generated one into .env.
|
|
2
|
+
# Never commit .env: the seed *is* your agent (address + wallet).
|
|
3
|
+
AGENT_SEED_PHRASE=
|
|
4
|
+
|
|
5
|
+
# ASI:One LLM key: https://asi1.ai/developer
|
|
6
|
+
ASI_ONE_API_KEY=
|
|
7
|
+
# ASI_ONE_MODEL=asi1
|
|
8
|
+
|
|
9
|
+
# Agentverse API key: https://agentverse.ai/profile/api-keys
|
|
10
|
+
# With it, the agent registers itself (name, @handle, description, README) on startup.
|
|
11
|
+
AGENTVERSE_API_KEY=
|
|
12
|
+
|
|
13
|
+
# Your unique @handle on Agentverse (max 40 chars). Check: uv run hackkit handle <name>
|
|
14
|
+
AGENT_HANDLE=__AGENT_HANDLE__
|
|
15
|
+
|
|
16
|
+
# AGENT_PORT=8000
|
|
17
|
+
# FET_NETWORK=stable-testnet # or mainnet (real money!)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"model": "asi1/asi1",
|
|
4
|
+
"small_model": "asi1/asi1-mini",
|
|
5
|
+
"provider": {
|
|
6
|
+
"asi1": {
|
|
7
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
8
|
+
"name": "ASI:One",
|
|
9
|
+
"options": {
|
|
10
|
+
"baseURL": "https://api.asi1.ai/v1",
|
|
11
|
+
"apiKey": "{env:ASI_ONE_API_KEY}"
|
|
12
|
+
},
|
|
13
|
+
"models": {
|
|
14
|
+
"asi1": {
|
|
15
|
+
"name": "ASI1",
|
|
16
|
+
"limit": {
|
|
17
|
+
"context": 196000,
|
|
18
|
+
"output": 40000
|
|
19
|
+
},
|
|
20
|
+
"modalities": {
|
|
21
|
+
"input": [
|
|
22
|
+
"text",
|
|
23
|
+
"image"
|
|
24
|
+
],
|
|
25
|
+
"output": [
|
|
26
|
+
"text"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"asi1-ultra": {
|
|
31
|
+
"name": "ASI1 Ultra",
|
|
32
|
+
"limit": {
|
|
33
|
+
"context": 196000,
|
|
34
|
+
"output": 40000
|
|
35
|
+
},
|
|
36
|
+
"modalities": {
|
|
37
|
+
"input": [
|
|
38
|
+
"text"
|
|
39
|
+
],
|
|
40
|
+
"output": [
|
|
41
|
+
"text"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"asi1-mini": {
|
|
46
|
+
"name": "ASI1 Mini",
|
|
47
|
+
"limit": {
|
|
48
|
+
"context": 196000,
|
|
49
|
+
"output": 40000
|
|
50
|
+
},
|
|
51
|
+
"modalities": {
|
|
52
|
+
"input": [
|
|
53
|
+
"text"
|
|
54
|
+
],
|
|
55
|
+
"output": [
|
|
56
|
+
"text"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"mcp": {
|
|
64
|
+
"agentverse": {
|
|
65
|
+
"type": "remote",
|
|
66
|
+
"url": "https://mcp.agentverse.ai/mcp",
|
|
67
|
+
"enabled": true,
|
|
68
|
+
"headers": {
|
|
69
|
+
"Authorization": "Bearer {env:AGENTVERSE_API_KEY}"
|
|
70
|
+
},
|
|
71
|
+
"oauth": false
|
|
72
|
+
},
|
|
73
|
+
"agentverse-lite": {
|
|
74
|
+
"type": "remote",
|
|
75
|
+
"url": "https://mcp-lite.agentverse.ai/mcp",
|
|
76
|
+
"enabled": false,
|
|
77
|
+
"headers": {
|
|
78
|
+
"Authorization": "Bearer {env:AGENTVERSE_API_KEY}"
|
|
79
|
+
},
|
|
80
|
+
"oauth": false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "__PROJECT_SLUG__"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "__AGENT_NAME__: a Fetch.ai agent built with fetch-hackkit"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = ["fetch-hackkit[mcp]>=__HACKKIT_VERSION__"]
|
|
7
|
+
|
|
8
|
+
[dependency-groups]
|
|
9
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.8"]
|
|
10
|
+
|
|
11
|
+
[tool.uv]
|
|
12
|
+
package = false
|
|
13
|
+
|
|
14
|
+
[tool.pytest.ini_options]
|
|
15
|
+
asyncio_mode = "auto"
|
|
16
|
+
|
|
17
|
+
[tool.ruff]
|
|
18
|
+
line-length = 100
|
|
19
|
+
|
|
20
|
+
[tool.ruff.format]
|
|
21
|
+
exclude = ["*.md"] # keep doc snippets compact
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Every card your agent can send must pass ASI:One's validation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
os.environ.setdefault("AGENT_SEED_PHRASE", "unit test seed phrase, not a real agent")
|
|
8
|
+
os.environ["AGENTVERSE_API_KEY"] = "" # never connect to Agentverse from tests
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
10
|
+
|
|
11
|
+
from fetch_hackkit.cards import collect_actions, validate
|
|
12
|
+
|
|
13
|
+
import agent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_menu_card_is_valid() -> None:
|
|
17
|
+
card = agent.menu()
|
|
18
|
+
assert validate(card.kind, card.payload) == []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_every_menu_button_has_a_handler() -> None:
|
|
22
|
+
actions = collect_actions(agent.menu().payload)
|
|
23
|
+
assert actions <= set(agent.agent._action_handlers)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fetch-hackkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Batteries for Fetch.ai hackathon agents: chat protocol, ASI:One interactive cards, ASI:One LLM + tools, payments, MCP, and Agentverse registration.
|
|
5
5
|
Project-URL: Homepage, https://github.com/adylagad/fetch-hackathon-starter
|
|
6
6
|
Project-URL: Documentation, https://github.com/adylagad/fetch-hackathon-starter/blob/main/AGENTS.md
|
|
@@ -34,6 +34,14 @@ Batteries for [Fetch.ai](https://fetch.ai) hackathon agents. Build a uAgent that
|
|
|
34
34
|
- **Multi-agent**: `ask_agent(ctx, "@handle", "question")`.
|
|
35
35
|
- **Agentverse**: mailbox auto-connect with name, `@handle`, description and README, plus the `hackkit doctor` pre-submission checks.
|
|
36
36
|
|
|
37
|
+
Start a new project in one command (fresh seed, a free `@handle`, README, tests, Dockerfile, AGENTS.md and coding-agent configs):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx --from "fetch-hackkit[mcp]" hackkit new "Trip Planner"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or add it to an existing project:
|
|
44
|
+
|
|
37
45
|
```bash
|
|
38
46
|
pip install fetch-hackkit # or: pip install "fetch-hackkit[mcp]"
|
|
39
47
|
```
|
|
@@ -63,6 +71,7 @@ Environment (`.env` is loaded automatically):
|
|
|
63
71
|
## CLI
|
|
64
72
|
|
|
65
73
|
```
|
|
74
|
+
hackkit new NAME create a ready-to-run agent project
|
|
66
75
|
hackkit seed generate a secure seed phrase (+ its address and wallet)
|
|
67
76
|
hackkit whoami address, wallet and Agentverse profile URL for your seed
|
|
68
77
|
hackkit handle NAME is @NAME free on Agentverse?
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
fetch_hackkit/__init__.py,sha256=NNakszX54xAS7I3ZR5GyTYkaR_wmKXcA-46qcJmicvk,729
|
|
2
|
+
fetch_hackkit/agent.py,sha256=06tUKS_tURCJelGw4JV57MaYR5rGdWSi_AALBGFahtg,12865
|
|
3
|
+
fetch_hackkit/asi1.py,sha256=CUH6GG7DqEQF3bC4srZ3Wlr6GGeKxi1J7Pkwxomf1N8,11321
|
|
4
|
+
fetch_hackkit/cards.py,sha256=nPYfLFFMkHz5HPmCt3CuhackYO0E49uUl7gLhFT2sCI,18618
|
|
5
|
+
fetch_hackkit/cli.py,sha256=bO1u8tB9fKkNqdM0-5tlm1vNkdsmbS9JfH108RsQgHE,4576
|
|
6
|
+
fetch_hackkit/doctor.py,sha256=HbyNLhM_VDTTmS3NhbNWCZzE0PSgNVORrgCkUYs3ZpY,7374
|
|
7
|
+
fetch_hackkit/mcp.py,sha256=ZdU_xTShDS50WTkvncARGxUXB0IgLxhfvxhI6m3l8ec,3273
|
|
8
|
+
fetch_hackkit/payments.py,sha256=k0M-n8Yh5HHKarVR1XpxHXIgWKHt5ZR5l5hef8wzJ3c,9780
|
|
9
|
+
fetch_hackkit/peers.py,sha256=pnfdw86KjAXtcJmKOtgsG0xQSmW22PAywH2967JaDf8,1399
|
|
10
|
+
fetch_hackkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
fetch_hackkit/register.py,sha256=ZTRsM-zfAvXg_7VtuE76TEXLLU2dk9fB_OCU5kjHj6k,9610
|
|
12
|
+
fetch_hackkit/scaffold.py,sha256=N5brR0wlln85st1NUN8Oe2QLkLFurwryFlmNOKB5zkk,4217
|
|
13
|
+
fetch_hackkit/templates/agents_reference.md,sha256=ENDB3RQtco5SpnMbVXN5OBXUSrB5pKD9BxcbWxBMCzg,14925
|
|
14
|
+
fetch_hackkit/templates/project/AGENTS.md.tmpl,sha256=ykkLCxyGpnOKFpjZaMtTDmSibyB9h6G1LXGoMKxkT5w,1517
|
|
15
|
+
fetch_hackkit/templates/project/Dockerfile.tmpl,sha256=JN5-2tpwSNV0_xlPQR11BxOH4XSFbAZCEApcSrDa_m8,454
|
|
16
|
+
fetch_hackkit/templates/project/README.md.tmpl,sha256=wCKvBlMH_hyAn6smVT446egLpcGBpYN2ghCAhMLUDjs,710
|
|
17
|
+
fetch_hackkit/templates/project/agent.py.tmpl,sha256=1Hesy0ugmGrWaO1PaK4-jLLWEd3FgQV970OmTlt3-Bc,2979
|
|
18
|
+
fetch_hackkit/templates/project/cursor_mcp.json.tmpl,sha256=xcWvISQgYp829sRXxfgobZMZhv2LQfVKXC4zT5xoMTM,185
|
|
19
|
+
fetch_hackkit/templates/project/dockerignore.tmpl,sha256=TB0T-secugdvbHbytIe7YR_8L9ypV33jUgsq6Elggnw,76
|
|
20
|
+
fetch_hackkit/templates/project/env.example.tmpl,sha256=jnk1n-FWL0RE_wCxm62vFUc0KdB5lLBp1KdTXL8QTW8,606
|
|
21
|
+
fetch_hackkit/templates/project/gitignore.tmpl,sha256=xDv5v0ZsuvzkrEl0XVsedXNR-fI9gkKHU4C5x0V1yYk,118
|
|
22
|
+
fetch_hackkit/templates/project/mcp.json.tmpl,sha256=_YNXCVwitkgPJ4Jp7RVvfBb8a6Nn5RszGsf8sSx0xLw,203
|
|
23
|
+
fetch_hackkit/templates/project/opencode.json.tmpl,sha256=tYnab34xxLakUf0Kr2GKqo2IbpuHkcvwmBxEPIi3FS8,1797
|
|
24
|
+
fetch_hackkit/templates/project/pyproject.toml.tmpl,sha256=OvXs6iJ35UULuyjr6D6ZM7OsCHlRw7DtdebIeu0qmV4,467
|
|
25
|
+
fetch_hackkit/templates/project/python-version.tmpl,sha256=Auc1s9_hwygz61ULf_j_oX9fK8P6HnuuYaj1o4g845g,5
|
|
26
|
+
fetch_hackkit/templates/project/tests/test_agent.py.tmpl,sha256=wofyDZyr48E9ZeDSHdjd-zECNRcff2b8EF2uJ8Y_0JI,695
|
|
27
|
+
fetch_hackkit-0.2.0.dist-info/METADATA,sha256=bLRlMhXG_zgKMKy5Wv1pC68erKz3IDX0kE0LQZG78Uc,3398
|
|
28
|
+
fetch_hackkit-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
29
|
+
fetch_hackkit-0.2.0.dist-info/entry_points.txt,sha256=q4jE2XuDrXcHO9S2PB2j-G4w1zaXu2I7KnZNnzGx-NY,51
|
|
30
|
+
fetch_hackkit-0.2.0.dist-info/licenses/LICENSE,sha256=5gBVHjA8rbq1fHVmCGulYLk0PujDeuPnj-TS9HnhhTc,1065
|
|
31
|
+
fetch_hackkit-0.2.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
fetch_hackkit/__init__.py,sha256=10LI3Rm0c-VmnMeweOoq37-_euhtjIRZaxhb6t-7-w4,729
|
|
2
|
-
fetch_hackkit/agent.py,sha256=06tUKS_tURCJelGw4JV57MaYR5rGdWSi_AALBGFahtg,12865
|
|
3
|
-
fetch_hackkit/asi1.py,sha256=CUH6GG7DqEQF3bC4srZ3Wlr6GGeKxi1J7Pkwxomf1N8,11321
|
|
4
|
-
fetch_hackkit/cards.py,sha256=nPYfLFFMkHz5HPmCt3CuhackYO0E49uUl7gLhFT2sCI,18618
|
|
5
|
-
fetch_hackkit/cli.py,sha256=9sbIxNTmT_Zr-5TaDMXdScDPfTYHS83lSZTee7tEEhY,3047
|
|
6
|
-
fetch_hackkit/doctor.py,sha256=HbyNLhM_VDTTmS3NhbNWCZzE0PSgNVORrgCkUYs3ZpY,7374
|
|
7
|
-
fetch_hackkit/mcp.py,sha256=ZdU_xTShDS50WTkvncARGxUXB0IgLxhfvxhI6m3l8ec,3273
|
|
8
|
-
fetch_hackkit/payments.py,sha256=k0M-n8Yh5HHKarVR1XpxHXIgWKHt5ZR5l5hef8wzJ3c,9780
|
|
9
|
-
fetch_hackkit/peers.py,sha256=pnfdw86KjAXtcJmKOtgsG0xQSmW22PAywH2967JaDf8,1399
|
|
10
|
-
fetch_hackkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
fetch_hackkit/register.py,sha256=ZTRsM-zfAvXg_7VtuE76TEXLLU2dk9fB_OCU5kjHj6k,9610
|
|
12
|
-
fetch_hackkit-0.1.0.dist-info/METADATA,sha256=fBofvP5BVsDD0dRWzLHxPsLE61bEvHrAffCEkfo0NxY,3100
|
|
13
|
-
fetch_hackkit-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
14
|
-
fetch_hackkit-0.1.0.dist-info/entry_points.txt,sha256=q4jE2XuDrXcHO9S2PB2j-G4w1zaXu2I7KnZNnzGx-NY,51
|
|
15
|
-
fetch_hackkit-0.1.0.dist-info/licenses/LICENSE,sha256=5gBVHjA8rbq1fHVmCGulYLk0PujDeuPnj-TS9HnhhTc,1065
|
|
16
|
-
fetch_hackkit-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|