-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy path__init__.py
More file actions
164 lines (135 loc) · 6.55 KB
/
Copy path__init__.py
File metadata and controls
164 lines (135 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"""Example protoAgent plugin.
A plugin is a directory with a ``protoagent.plugin.yaml`` manifest and a module
exposing ``register(registry)``. The registry collects what the plugin
contributes. This example shows **all five** contribution types (ADR 0001 + 0018):
- a **tool** (``hello``),
- a bundled **SKILL.md** directory (``skills/``),
- an HTTP **route** (``GET /plugins/hello/ping``),
- a lifecycle **surface** (logs on startup/shutdown),
- a **subagent** (``hello_helper``).
Enable it with ``plugins: { enabled: [hello] }`` in config. A fork drops a
directory like this in ``plugins/`` and never edits core ``server.py``.
"""
from __future__ import annotations
import logging
from langchain_core.tools import tool
log = logging.getLogger("protoagent.plugins.hello")
@tool
async def hello(name: str = "world") -> str:
"""Return a friendly greeting — proof the plugin loaded and its tool is live."""
return f"Hello, {name}! (from the example protoAgent plugin)"
def _build_router(greeting: str):
"""A tiny FastAPI router — mounted at ``/plugins/hello`` (ADR 0018). Closes
over the plugin's configured greeting (ADR 0019) to prove it reads its config."""
from fastapi import APIRouter
router = APIRouter()
@router.get("/ping")
async def _ping() -> dict:
return {"ok": True, "plugin": "hello", "greeting": greeting}
# A console view (ADR 0026): the manifest's `views:` entry points the rail
# iframe here. A plugin serves whatever UI it wants; this demo is a tiny
# self-contained page that matches the console's dark ground.
@router.get("/view")
async def _view():
from fastapi.responses import HTMLResponse
# The page listens for the console's `protoagent:init` postMessage (ADR
# 0026 bridge) — the operator bearer (use it for your own API calls) + the
# console theme tokens (apply them to match the look). Reference receiver.
html = f"""<!doctype html><html><head><meta charset="utf-8">
<style>
html,body{{margin:0;height:100%;background:#0a0f14;color:#e6e6e6;
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;
display:flex;align-items:center;justify-content:center;text-align:center}}
.card{{padding:32px}} h1{{color:#9b87f2;margin:0 0 8px;font-size:22px}}
p{{color:#9aa0aa;font-size:14px;max-width:38ch;line-height:1.6}}
code{{background:#161b22;padding:2px 6px;border-radius:5px;color:#9b87f2}}
#bridge{{margin-top:14px;font-size:12px;color:#46c46a}}
</style></head><body><div class="card">
<h1>{greeting} from a plugin view</h1>
<p>Served by <code>plugins/hello</code> at <code>/plugins/hello/view</code> and
embedded in the console rail via the <code>views:</code> manifest block.
A fork drops a directory like this and gets its own rail icon +
dashboard — no console rebuild.</p>
<p id="bridge">awaiting console handshake…</p>
</div>
<script>
function applyTheme(t) {{
if (!t) return;
if (t.bg) document.body.style.background = t.bg;
if (t.fg) document.body.style.color = t.fg;
}}
window.addEventListener("message", function (e) {{
var m = e.data || {{}};
if (m.type === "protoagent:init") {{
document.getElementById("bridge").textContent =
(m.token ? "✓ authed by the console" : "no token (open API)") +
(m.theme ? " · theme received" : "");
applyTheme(m.theme);
}} else if (m.type === "protoagent:theme") {{
// Live theme update — the console re-posts on any edit / agent switch.
applyTheme(m.theme);
}}
}});
</script>
</body></html>"""
return HTMLResponse(html)
return router
async def _surface_start() -> None:
"""A no-op lifecycle surface — a real one would open a gateway/listener here
(it runs on the server loop, like the Discord gateway)."""
log.info("[hello] example surface started")
async def _surface_stop() -> None:
log.info("[hello] example surface stopped")
def _build_subagent():
"""A minimal delegate the lead agent can call via ``task``/``task_batch``."""
from graph.subagents.config import SubagentConfig
return SubagentConfig(
name="hello_helper",
description="Example plugin subagent — echoes a friendly status. Proof a "
"plugin can register a delegate without editing SUBAGENT_REGISTRY.",
system_prompt="You are the hello_helper, a tiny example subagent. Reply briefly and cheerfully, then stop.",
tools=["current_time"],
)
async def _hello_form_command(rest: str, session_id: str):
"""A user-only chat command that opens a FORM instead of replying (#1701 Slice 2):
`/hello-form` shows a one-field picker in the composer; submitting it echoes the
choice back as a note — the plugin↔composer-form round-trip in ~20 lines."""
async def _on_submit(answers: dict, _session_id: str) -> str:
style = str(answers.get("style") or "friendly")
return f"Hello — in a **{style}** style! (from the example plugin's form)"
return {
"form": {
"kind": "form",
"title": "Say hello",
"description": "Pick a greeting style.",
"steps": [
{
"schema": {
"type": "object",
"required": ["style"],
"properties": {
"style": {
"type": "string",
"title": "Style",
"oneOf": [
{"const": "friendly", "title": "friendly", "description": "warm and casual"},
{"const": "formal", "title": "formal", "description": "polished and precise"},
{"const": "pirate", "title": "pirate", "description": "arr, matey"},
],
}
},
}
}
],
},
"on_submit": _on_submit,
}
def register(registry) -> None:
"""Entry point — called once at load with a PluginRegistry."""
greeting = registry.config.get("greeting", "Hello") # plugin config (ADR 0019)
registry.register_tool(hello)
registry.register_skill_dir("skills") # bundled SKILL.md folder
registry.register_router(_build_router(greeting)) # → /plugins/hello/ping (ADR 0018)
registry.register_surface(_surface_start, stop=_surface_stop, name="hello-surface")
registry.register_subagent(_build_subagent())
registry.register_chat_command("hello-form", _hello_form_command) # #1701 Slice 2 demo