ak5 1.0.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.
- ak5/__init__.py +3 -0
- ak5/cli/__init__.py +3 -0
- ak5/cli/commands/agents.py +55 -0
- ak5/cli/commands/board.py +122 -0
- ak5/cli/commands/delegate.py +66 -0
- ak5/cli/commands/demo.py +230 -0
- ak5/cli/commands/login.py +47 -0
- ak5/cli/commands/mcp.py +8 -0
- ak5/cli/commands/serve.py +18 -0
- ak5/cli/config.py +35 -0
- ak5/cli/main.py +31 -0
- ak5/config.py +29 -0
- ak5/database.py +52 -0
- ak5/main.py +148 -0
- ak5/mcp/__init__.py +17 -0
- ak5/mcp/client.py +164 -0
- ak5/mcp/server.py +10 -0
- ak5/mcp/tools.py +149 -0
- ak5/models/__init__.py +16 -0
- ak5/models/actor.py +50 -0
- ak5/models/audit.py +38 -0
- ak5/models/base.py +24 -0
- ak5/models/board.py +40 -0
- ak5/models/column.py +46 -0
- ak5/models/ticket.py +157 -0
- ak5/routers/__init__.py +13 -0
- ak5/routers/actors.py +101 -0
- ak5/routers/auth.py +137 -0
- ak5/routers/boards.py +211 -0
- ak5/routers/events.py +37 -0
- ak5/routers/tickets.py +488 -0
- ak5/schemas/__init__.py +57 -0
- ak5/schemas/actor.py +47 -0
- ak5/schemas/audit.py +16 -0
- ak5/schemas/board.py +31 -0
- ak5/schemas/column.py +23 -0
- ak5/schemas/event.py +10 -0
- ak5/schemas/ticket.py +93 -0
- ak5/services/__init__.py +12 -0
- ak5/services/discovery.py +79 -0
- ak5/services/event_bus.py +72 -0
- ak5/services/lexorank.py +149 -0
- ak5-1.0.0.dist-info/METADATA +163 -0
- ak5-1.0.0.dist-info/RECORD +46 -0
- ak5-1.0.0.dist-info/WHEEL +4 -0
- ak5-1.0.0.dist-info/entry_points.txt +3 -0
ak5/__init__.py
ADDED
ak5/cli/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import httpx
|
|
3
|
+
from ak5.cli.config import get_api_url
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("agents")
|
|
11
|
+
@click.option("--cap", default=None, help="Filter by capability tag (e.g. 'image-resize')")
|
|
12
|
+
@click.option("--status", default=None, help="Filter by status ('idle', 'busy', 'offline')")
|
|
13
|
+
@click.option("--query", default=None, help="Semantic search query")
|
|
14
|
+
def agents_command(cap: str | None, status: str | None, query: str | None) -> None:
|
|
15
|
+
"""Discover agents matching capability tags or search queries."""
|
|
16
|
+
api_url = get_api_url()
|
|
17
|
+
params = {}
|
|
18
|
+
if cap:
|
|
19
|
+
params["capability"] = cap
|
|
20
|
+
if status:
|
|
21
|
+
params["status"] = status
|
|
22
|
+
if query:
|
|
23
|
+
params["query"] = query
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with httpx.Client(timeout=10.0) as client:
|
|
27
|
+
resp = client.get(f"{api_url}/actors/discovery", params=params)
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
agents = resp.json()
|
|
30
|
+
|
|
31
|
+
if not agents:
|
|
32
|
+
console.print("[yellow]No matching agents found.[/yellow]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
table = Table(title="Available Autonomous AI Agents", show_lines=True)
|
|
36
|
+
table.add_column("Agent ID", style="cyan", no_wrap=True)
|
|
37
|
+
table.add_column("Role", style="magenta")
|
|
38
|
+
table.add_column("Status", style="green")
|
|
39
|
+
table.add_column("Capabilities", style="yellow")
|
|
40
|
+
|
|
41
|
+
for a in agents:
|
|
42
|
+
caps_str = ", ".join(a.get("capabilities", []))
|
|
43
|
+
status_color = "green" if a["status"] == "idle" else ("yellow" if a["status"] == "busy" else "red")
|
|
44
|
+
table.add_row(
|
|
45
|
+
f"@{a['actor_id']}",
|
|
46
|
+
a["role"],
|
|
47
|
+
f"[{status_color}]{a['status']}[/{status_color}]",
|
|
48
|
+
caps_str,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
console.print(table)
|
|
52
|
+
except httpx.ConnectError:
|
|
53
|
+
console.print(f"[bold red]✗ Failed to connect to AK5 Gateway at {api_url}[/bold red]")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
console.print(f"[bold red]✗ Error querying agents:[/bold red] {e}")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import httpx
|
|
5
|
+
from ak5.cli.config import get_api_url
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_board_view(board_data: dict) -> Table:
|
|
16
|
+
"""Render 4-column Kanban board as a Rich Table."""
|
|
17
|
+
main_table = Table(
|
|
18
|
+
title=f"📋 Kanban Board: {board_data['name']} ({board_data['board_id']})",
|
|
19
|
+
show_header=True,
|
|
20
|
+
header_style="bold cyan",
|
|
21
|
+
expand=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
stage_colors = {
|
|
25
|
+
"open": "blue",
|
|
26
|
+
"in_progress": "yellow",
|
|
27
|
+
"review": "magenta",
|
|
28
|
+
"done": "green",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Add column headers
|
|
32
|
+
for col in board_data.get("columns", []):
|
|
33
|
+
color = stage_colors.get(col.get("stage"), "white")
|
|
34
|
+
count = len(col.get("tickets", []))
|
|
35
|
+
wip_info = f" [WIP: {count}/{col['wip_limit']}]" if col.get("wip_limit", 0) > 0 else f" ({count})"
|
|
36
|
+
main_table.add_column(f"[{color}]{col['name']}{wip_info}[/{color}]")
|
|
37
|
+
|
|
38
|
+
# Find maximum number of rows among columns
|
|
39
|
+
max_tickets = max(len(col.get("tickets", [])) for col in board_data.get("columns", [])) if board_data.get("columns") else 0
|
|
40
|
+
|
|
41
|
+
if max_tickets == 0:
|
|
42
|
+
empty_row = [Panel("[dim](Empty)[/dim]", expand=True) for _ in board_data.get("columns", [])]
|
|
43
|
+
main_table.add_row(*empty_row)
|
|
44
|
+
return main_table
|
|
45
|
+
|
|
46
|
+
for row_idx in range(max_tickets):
|
|
47
|
+
row_cells = []
|
|
48
|
+
for col in board_data.get("columns", []):
|
|
49
|
+
tickets = col.get("tickets", [])
|
|
50
|
+
if row_idx < len(tickets):
|
|
51
|
+
t = tickets[row_idx]
|
|
52
|
+
p_color = "red" if t["priority"] in ("urgent", "high") else ("yellow" if t["priority"] == "medium" else "dim")
|
|
53
|
+
status_badge = f"[{p_color}][{t['priority'].upper()}][/{p_color}]"
|
|
54
|
+
|
|
55
|
+
assignee = f"@{t['assigned_to']}" if t.get("assigned_to") else "[dim]Unassigned[/dim]"
|
|
56
|
+
|
|
57
|
+
subtask_badge = ""
|
|
58
|
+
if t.get("subtask_count", 0) > 0:
|
|
59
|
+
subtask_badge = f"\n[bold cyan]Subtasks: {t['subtask_done_count']}/{t['subtask_count']} Done[/bold cyan]"
|
|
60
|
+
|
|
61
|
+
content = (
|
|
62
|
+
f"[bold white]{t['title']}[/bold white]\n"
|
|
63
|
+
f"{status_badge} [dim]{t['ticket_id']}[/dim] | {assignee}"
|
|
64
|
+
f"{subtask_badge}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
border_color = "yellow" if col.get("stage") == "in_progress" else "white"
|
|
68
|
+
row_cells.append(Panel(content, border_style=border_color, expand=True))
|
|
69
|
+
else:
|
|
70
|
+
row_cells.append(Text(""))
|
|
71
|
+
main_table.add_row(*row_cells)
|
|
72
|
+
|
|
73
|
+
return main_table
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def fetch_board(api_url: str, board_id: str) -> dict:
|
|
77
|
+
with httpx.Client(timeout=10.0) as client:
|
|
78
|
+
resp = client.get(f"{api_url}/boards/{board_id}")
|
|
79
|
+
resp.raise_for_status()
|
|
80
|
+
return resp.json()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def watch_board_live(api_url: str, board_id: str) -> None:
|
|
84
|
+
"""Stream SSE events and re-render board on changes."""
|
|
85
|
+
console.print(f"[bold cyan]Connecting to real-time event stream for board '{board_id}'...[/bold cyan]")
|
|
86
|
+
board_data = fetch_board(api_url, board_id)
|
|
87
|
+
|
|
88
|
+
with Live(render_board_view(board_data), console=console, refresh_per_second=4) as live:
|
|
89
|
+
async with (
|
|
90
|
+
httpx.AsyncClient(timeout=None) as client,
|
|
91
|
+
client.stream("GET", f"{api_url}/events/stream") as stream,
|
|
92
|
+
):
|
|
93
|
+
async for line in stream.aiter_lines():
|
|
94
|
+
if line.startswith("event:"):
|
|
95
|
+
event_type = line.split(":", 1)[1].strip()
|
|
96
|
+
if event_type in ("TICKET_CREATED", "TICKET_MOVED", "TICKET_DELEGATED", "TICKET_UPDATED", "COMMENT_ADDED"):
|
|
97
|
+
# Refresh board
|
|
98
|
+
try:
|
|
99
|
+
board_data = fetch_board(api_url, board_id)
|
|
100
|
+
live.update(render_board_view(board_data))
|
|
101
|
+
except (httpx.HTTPError, OSError):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@click.command("board")
|
|
106
|
+
@click.option("--board-id", default="proj-core-engine", help="Target Board ID")
|
|
107
|
+
@click.option("--watch", is_flag=True, help="Watch board with live real-time SSE updates")
|
|
108
|
+
def board_command(board_id: str, watch: bool) -> None:
|
|
109
|
+
"""View Kanban board in terminal."""
|
|
110
|
+
api_url = get_api_url()
|
|
111
|
+
try:
|
|
112
|
+
if watch:
|
|
113
|
+
asyncio.run(watch_board_live(api_url, board_id))
|
|
114
|
+
else:
|
|
115
|
+
board_data = fetch_board(api_url, board_id)
|
|
116
|
+
console.print(render_board_view(board_data))
|
|
117
|
+
except httpx.ConnectError:
|
|
118
|
+
console.print(f"[bold red]✗ Failed to connect to AK5 Gateway at {api_url}[/bold red]")
|
|
119
|
+
except httpx.HTTPStatusError as e:
|
|
120
|
+
console.print(f"[bold red]✗ Board query failed ({e.response.status_code}):[/bold red] {e.response.text}")
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
console.print("\n[yellow]Watch stopped.[/yellow]")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import httpx
|
|
3
|
+
from ak5.cli.config import get_api_url, get_token
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("delegate")
|
|
10
|
+
@click.argument("ticket_id")
|
|
11
|
+
@click.option("--to", "target_actor_id", required=True, help="Target Agent ID to delegate task to")
|
|
12
|
+
@click.option("--title", required=True, help="Subtask title")
|
|
13
|
+
@click.option("--desc", "description", default="", help="Subtask detailed description")
|
|
14
|
+
@click.option("--priority", type=click.Choice(["low", "medium", "high", "urgent"]), default="medium")
|
|
15
|
+
def delegate_command(
|
|
16
|
+
ticket_id: str,
|
|
17
|
+
target_actor_id: str,
|
|
18
|
+
title: str,
|
|
19
|
+
description: str,
|
|
20
|
+
priority: str,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Delegate a subtask to an agent under an existing parent ticket."""
|
|
23
|
+
api_url = get_api_url()
|
|
24
|
+
token = get_token()
|
|
25
|
+
|
|
26
|
+
headers = {}
|
|
27
|
+
if token:
|
|
28
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
29
|
+
else:
|
|
30
|
+
# Auto-login as orchestrator if not logged in
|
|
31
|
+
with httpx.Client(timeout=5.0) as client:
|
|
32
|
+
resp = client.post(
|
|
33
|
+
f"{api_url}/auth/identify",
|
|
34
|
+
json={"actor_id": "cli_user", "actor_type": "human", "name": "CLI User", "role": "PM"},
|
|
35
|
+
)
|
|
36
|
+
if resp.is_success:
|
|
37
|
+
headers["Authorization"] = f"Bearer {resp.json()['access_token']}"
|
|
38
|
+
|
|
39
|
+
payload = {
|
|
40
|
+
"target_actor_id": target_actor_id,
|
|
41
|
+
"subtask_title": title,
|
|
42
|
+
"subtask_description": description,
|
|
43
|
+
"priority": priority,
|
|
44
|
+
"labels": ["delegated", "cli"],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
with httpx.Client(timeout=10.0) as client:
|
|
49
|
+
resp = client.post(
|
|
50
|
+
f"{api_url}/tickets/{ticket_id}/delegate",
|
|
51
|
+
json=payload,
|
|
52
|
+
headers=headers,
|
|
53
|
+
)
|
|
54
|
+
resp.raise_for_status()
|
|
55
|
+
subtask = resp.json()
|
|
56
|
+
|
|
57
|
+
console.print(
|
|
58
|
+
f"[bold green]✓[/bold green] Subtask [cyan]{subtask['ticket_id']}[/cyan] created and assigned to "
|
|
59
|
+
f"[magenta]@{target_actor_id}[/magenta] (Parent: [yellow]{ticket_id}[/yellow])"
|
|
60
|
+
)
|
|
61
|
+
except httpx.ConnectError:
|
|
62
|
+
console.print(f"[bold red]✗ Failed to connect to AK5 Gateway at {api_url}[/bold red]")
|
|
63
|
+
except httpx.HTTPStatusError as e:
|
|
64
|
+
console.print(f"[bold red]✗ Delegation failed ({e.response.status_code}):[/bold red] {e.response.text}")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
console.print(f"[bold red]✗ Error:[/bold red] {e}")
|
ak5/cli/commands/demo.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import httpx
|
|
5
|
+
from ak5.cli.config import get_api_url
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def run_collaboration_simulation(api_url: str) -> None:
|
|
13
|
+
"""Run E2E autonomous multi-agent collaboration scenario.
|
|
14
|
+
|
|
15
|
+
1. Human PM creates a feature ticket: "Profile Image Optimization System"
|
|
16
|
+
2. Orchestrator Agent inspects ticket, discovers available agents via capability search
|
|
17
|
+
3. Orchestrator delegates Subtask 1 to @agent_image_worker ("WebP Conversion Routine")
|
|
18
|
+
4. Orchestrator delegates Subtask 2 to @agent_code_reviewer ("Review & Security Audit")
|
|
19
|
+
5. @agent_image_worker moves subtask to In Progress, writes execution context, moves to Done
|
|
20
|
+
6. @agent_code_reviewer reviews subtask, signs off with comment, moves to Done
|
|
21
|
+
7. Orchestrator verifies all subtasks are done, closes parent ticket to Done.
|
|
22
|
+
"""
|
|
23
|
+
console.print(Panel.fit(
|
|
24
|
+
"[bold cyan]AK5 Autonomous Multi-Agent Collaboration Simulation[/bold cyan]\n"
|
|
25
|
+
"[dim]Human PM ➔ Orchestrator Agent ➔ Image Worker Agent & Reviewer Agent[/dim]",
|
|
26
|
+
border_style="cyan",
|
|
27
|
+
))
|
|
28
|
+
|
|
29
|
+
async with httpx.AsyncClient(base_url=api_url, timeout=30.0) as client:
|
|
30
|
+
# Step 0: Ensure actors
|
|
31
|
+
console.print("\n[bold yellow][Phase 0] Registering Actors...[/bold yellow]")
|
|
32
|
+
for actor in [
|
|
33
|
+
{"id": "user_pm", "type": "human", "name": "David (PM)", "role": "PM", "caps": ["planning"]},
|
|
34
|
+
{"id": "agent_orchestrator", "type": "agent", "name": "Orchestrator Agent", "role": "Lead Architect", "caps": ["orchestration", "breakdown"]},
|
|
35
|
+
{"id": "agent_image_worker", "type": "agent", "name": "Image Worker", "role": "Media Specialist", "caps": ["image-resize", "webp"]},
|
|
36
|
+
{"id": "agent_code_reviewer", "type": "agent", "name": "Code Reviewer", "role": "Senior Reviewer", "caps": ["code-review", "security"]},
|
|
37
|
+
]:
|
|
38
|
+
resp = await client.post("/auth/identify", json={
|
|
39
|
+
"actor_id": actor["id"],
|
|
40
|
+
"actor_type": actor["type"],
|
|
41
|
+
"name": actor["name"],
|
|
42
|
+
"role": actor["role"],
|
|
43
|
+
"capabilities": actor["caps"],
|
|
44
|
+
})
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
token = resp.json()["access_token"]
|
|
47
|
+
if actor["id"] == "user_pm":
|
|
48
|
+
pm_token = token
|
|
49
|
+
elif actor["id"] == "agent_orchestrator":
|
|
50
|
+
orch_token = token
|
|
51
|
+
elif actor["id"] == "agent_image_worker":
|
|
52
|
+
worker_token = token
|
|
53
|
+
elif actor["id"] == "agent_code_reviewer":
|
|
54
|
+
reviewer_token = token
|
|
55
|
+
console.print(f" ✓ Actor @{actor['id']} active [{actor['type']}]")
|
|
56
|
+
|
|
57
|
+
board_id = "proj-core-engine"
|
|
58
|
+
board_resp = await client.get(f"/boards/{board_id}")
|
|
59
|
+
board_resp.raise_for_status()
|
|
60
|
+
board_data = board_resp.json()
|
|
61
|
+
col_map = {c["stage"]: c["column_id"] for c in board_data["columns"]}
|
|
62
|
+
|
|
63
|
+
# Step 1: Human PM registers master ticket
|
|
64
|
+
console.print("\n[bold yellow][Step 1] Human PM registers master ticket...[/bold yellow]")
|
|
65
|
+
t_resp = await client.post(
|
|
66
|
+
"/tickets",
|
|
67
|
+
json={
|
|
68
|
+
"title": "Profile Image Optimization System",
|
|
69
|
+
"description": "Implement automated avatar resizing to 200x200 WebP format with security audit.",
|
|
70
|
+
"board_id": board_id,
|
|
71
|
+
"column_id": col_map["open"],
|
|
72
|
+
"priority": "high",
|
|
73
|
+
"assigned_to": "agent_orchestrator",
|
|
74
|
+
"labels": ["avatar", "optimization"],
|
|
75
|
+
},
|
|
76
|
+
headers={"Authorization": f"Bearer {pm_token}"},
|
|
77
|
+
)
|
|
78
|
+
t_resp.raise_for_status()
|
|
79
|
+
parent_ticket = t_resp.json()
|
|
80
|
+
parent_id = parent_ticket["ticket_id"]
|
|
81
|
+
console.print(f" ✓ Created Parent Ticket: [cyan]{parent_id}[/cyan] ('{parent_ticket['title']}')")
|
|
82
|
+
await asyncio.sleep(1)
|
|
83
|
+
|
|
84
|
+
# Step 2: Orchestrator agent picks up ticket & moves to In Progress
|
|
85
|
+
console.print("\n[bold yellow][Step 2] Orchestrator analyzes requirements & searches capabilities...[/bold yellow]")
|
|
86
|
+
await client.patch(
|
|
87
|
+
f"/tickets/{parent_id}/move",
|
|
88
|
+
json={"target_column_id": col_map["in_progress"]},
|
|
89
|
+
headers={"Authorization": f"Bearer {orch_token}"},
|
|
90
|
+
)
|
|
91
|
+
console.print(f" ✓ Ticket {parent_id} moved to [yellow]In Progress[/yellow] by @agent_orchestrator")
|
|
92
|
+
|
|
93
|
+
# Discovery query: find image worker
|
|
94
|
+
disc_img = (await client.get("/actors/discovery?capability=image-resize")).json()
|
|
95
|
+
image_agent_id = disc_img[0]["actor_id"]
|
|
96
|
+
console.print(f" ✓ Discovered specialist agent for image optimization: [magenta]@{image_agent_id}[/magenta]")
|
|
97
|
+
|
|
98
|
+
# Discovery query: find reviewer
|
|
99
|
+
disc_rev = (await client.get("/actors/discovery?capability=code-review")).json()
|
|
100
|
+
reviewer_agent_id = disc_rev[0]["actor_id"]
|
|
101
|
+
console.print(f" ✓ Discovered specialist agent for review: [magenta]@{reviewer_agent_id}[/magenta]")
|
|
102
|
+
await asyncio.sleep(1)
|
|
103
|
+
|
|
104
|
+
# Step 3: Orchestrator delegates Subtask 1 to Image Worker
|
|
105
|
+
console.print("\n[bold yellow][Step 3] Orchestrator delegates Subtask 1 to Worker...[/bold yellow]")
|
|
106
|
+
del1_resp = await client.post(
|
|
107
|
+
f"/tickets/{parent_id}/delegate",
|
|
108
|
+
json={
|
|
109
|
+
"target_actor_id": image_agent_id,
|
|
110
|
+
"subtask_title": "Implement WebP Converter Routine",
|
|
111
|
+
"subtask_description": "Convert raw PNG/JPEG buffer to 200x200 WebP with 85% quality factor.",
|
|
112
|
+
"priority": "high",
|
|
113
|
+
"labels": ["worker", "webp"],
|
|
114
|
+
},
|
|
115
|
+
headers={"Authorization": f"Bearer {orch_token}"},
|
|
116
|
+
)
|
|
117
|
+
sub1 = del1_resp.json()
|
|
118
|
+
sub1_id = sub1["ticket_id"]
|
|
119
|
+
console.print(f" ✓ Subtask [cyan]{sub1_id}[/cyan] delegated to [magenta]@{image_agent_id}[/magenta]")
|
|
120
|
+
await asyncio.sleep(1)
|
|
121
|
+
|
|
122
|
+
# Step 4: Worker processes Subtask 1
|
|
123
|
+
console.print("\n[bold yellow][Step 4] Image Worker processes Subtask 1...[/bold yellow]")
|
|
124
|
+
await client.patch(
|
|
125
|
+
f"/tickets/{sub1_id}/move",
|
|
126
|
+
json={"target_column_id": col_map["in_progress"]},
|
|
127
|
+
headers={"Authorization": f"Bearer {worker_token}"},
|
|
128
|
+
)
|
|
129
|
+
console.print(f" ✓ {sub1_id} status ➔ [yellow]In Progress[/yellow]")
|
|
130
|
+
|
|
131
|
+
await client.post(
|
|
132
|
+
f"/tickets/{sub1_id}/comments",
|
|
133
|
+
json={
|
|
134
|
+
"content": "Generated PIL image transform logic; benchmarked at 12ms per 1024x1024 input.",
|
|
135
|
+
"is_internal": True,
|
|
136
|
+
"metadata": {"execution_time_ms": 12, "compression_ratio": "78%"},
|
|
137
|
+
},
|
|
138
|
+
headers={"Authorization": f"Bearer {worker_token}"},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
await client.patch(
|
|
142
|
+
f"/tickets/{sub1_id}/move",
|
|
143
|
+
json={"target_column_id": col_map["done"]},
|
|
144
|
+
headers={"Authorization": f"Bearer {worker_token}"},
|
|
145
|
+
)
|
|
146
|
+
console.print(f" ✓ {sub1_id} status ➔ [green]Done[/green] with artifact context")
|
|
147
|
+
await asyncio.sleep(1)
|
|
148
|
+
|
|
149
|
+
# Step 5: Orchestrator delegates Subtask 2 to Code Reviewer
|
|
150
|
+
console.print("\n[bold yellow][Step 5] Orchestrator delegates Subtask 2 for Review...[/bold yellow]")
|
|
151
|
+
del2_resp = await client.post(
|
|
152
|
+
f"/tickets/{parent_id}/delegate",
|
|
153
|
+
json={
|
|
154
|
+
"target_actor_id": reviewer_agent_id,
|
|
155
|
+
"subtask_title": "Security & Boundary Review for WebP Converter",
|
|
156
|
+
"subtask_description": "Audit buffer overflow checks and decompression bomb protection.",
|
|
157
|
+
"priority": "urgent",
|
|
158
|
+
"labels": ["security", "audit"],
|
|
159
|
+
},
|
|
160
|
+
headers={"Authorization": f"Bearer {orch_token}"},
|
|
161
|
+
)
|
|
162
|
+
sub2 = del2_resp.json()
|
|
163
|
+
sub2_id = sub2["ticket_id"]
|
|
164
|
+
console.print(f" ✓ Subtask [cyan]{sub2_id}[/cyan] delegated to [magenta]@{reviewer_agent_id}[/magenta]")
|
|
165
|
+
await asyncio.sleep(1)
|
|
166
|
+
|
|
167
|
+
# Step 6: Code Reviewer processes Subtask 2
|
|
168
|
+
console.print("\n[bold yellow][Step 6] Code Reviewer audits implementation...[/bold yellow]")
|
|
169
|
+
await client.patch(
|
|
170
|
+
f"/tickets/{sub2_id}/move",
|
|
171
|
+
json={"target_column_id": col_map["in_progress"]},
|
|
172
|
+
headers={"Authorization": f"Bearer {reviewer_token}"},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
await client.post(
|
|
176
|
+
f"/tickets/{sub2_id}/comments",
|
|
177
|
+
json={
|
|
178
|
+
"content": "LGTM! Input pixel limit set to 4096 to prevent memory denial-of-service. Approved.",
|
|
179
|
+
"is_internal": False,
|
|
180
|
+
"metadata": {"approved": True, "cve_checked": ["CVE-2023-4863"]},
|
|
181
|
+
},
|
|
182
|
+
headers={"Authorization": f"Bearer {reviewer_token}"},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
await client.patch(
|
|
186
|
+
f"/tickets/{sub2_id}/move",
|
|
187
|
+
json={"target_column_id": col_map["done"]},
|
|
188
|
+
headers={"Authorization": f"Bearer {reviewer_token}"},
|
|
189
|
+
)
|
|
190
|
+
console.print(f" ✓ {sub2_id} status ➔ [green]Done[/green]")
|
|
191
|
+
await asyncio.sleep(1)
|
|
192
|
+
|
|
193
|
+
# Step 7: Orchestrator closes Parent Ticket
|
|
194
|
+
console.print("\n[bold yellow][Step 7] Orchestrator closes Parent Ticket...[/bold yellow]")
|
|
195
|
+
final_check = (await client.get(f"/tickets/{parent_id}")).json()
|
|
196
|
+
console.print(f" Subtasks completion status: [green]{final_check['subtask_done_count']}/{final_check['subtask_count']} Done[/green]")
|
|
197
|
+
|
|
198
|
+
await client.patch(
|
|
199
|
+
f"/tickets/{parent_id}/move",
|
|
200
|
+
json={"target_column_id": col_map["done"]},
|
|
201
|
+
headers={"Authorization": f"Bearer {orch_token}"},
|
|
202
|
+
)
|
|
203
|
+
await client.post(
|
|
204
|
+
f"/tickets/{parent_id}/comments",
|
|
205
|
+
json={
|
|
206
|
+
"content": "All subtasks (implementation + security review) successfully completed. Closing ticket.",
|
|
207
|
+
"is_internal": False,
|
|
208
|
+
},
|
|
209
|
+
headers={"Authorization": f"Bearer {orch_token}"},
|
|
210
|
+
)
|
|
211
|
+
console.print(f" ✓ Master Ticket [cyan]{parent_id}[/cyan] successfully resolved and moved to [green]Done[/green]!")
|
|
212
|
+
|
|
213
|
+
console.print(Panel.fit(
|
|
214
|
+
"[bold green]🎉 E2E Collaboration Simulation Completed Successfully![/bold green]\n"
|
|
215
|
+
f"View live board: [cyan]ak5 board --board-id {board_id}[/cyan]",
|
|
216
|
+
border_style="green",
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@click.command("demo")
|
|
221
|
+
def demo_command() -> None:
|
|
222
|
+
"""Run automated autonomous multi-agent collaboration demonstration."""
|
|
223
|
+
api_url = get_api_url()
|
|
224
|
+
try:
|
|
225
|
+
asyncio.run(run_collaboration_simulation(api_url))
|
|
226
|
+
except httpx.ConnectError:
|
|
227
|
+
console.print(f"[bold red]✗ Cannot connect to AK5 Gateway at {api_url}[/bold red]")
|
|
228
|
+
console.print("Please start the backend server in another terminal:\n [yellow]uv run uvicorn ak5.main:app[/yellow]")
|
|
229
|
+
except Exception as e:
|
|
230
|
+
console.print(f"[bold red]✗ Simulation failed:[/bold red] {e}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import httpx
|
|
3
|
+
from ak5.cli.config import get_api_url, save_session
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("login")
|
|
10
|
+
@click.option("--id", "actor_id", required=True, help="Actor ID (e.g. agent-code-reviewer, user_pm)")
|
|
11
|
+
@click.option("--role", required=True, help="Display role (e.g. 'Senior Reviewer', 'PM')")
|
|
12
|
+
@click.option("--caps", default="", help="Comma-separated capabilities (e.g. 'python,rust,security')")
|
|
13
|
+
@click.option("--type", "actor_type", type=click.Choice(["human", "agent"]), default="agent", help="Actor type")
|
|
14
|
+
@click.option("--url", default=None, help="AK5 Gateway API URL")
|
|
15
|
+
def login_command(actor_id: str, role: str, caps: str, actor_type: str, url: str | None) -> None:
|
|
16
|
+
"""Identify and authenticate as an Actor (Human or AI Agent)."""
|
|
17
|
+
api_url = url or get_api_url()
|
|
18
|
+
capabilities = [c.strip() for c in caps.split(",") if c.strip()]
|
|
19
|
+
|
|
20
|
+
payload = {
|
|
21
|
+
"actor_id": actor_id,
|
|
22
|
+
"actor_type": actor_type,
|
|
23
|
+
"name": actor_id.replace("-", " ").replace("_", " ").title(),
|
|
24
|
+
"role": role,
|
|
25
|
+
"capabilities": capabilities,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
with httpx.Client(timeout=10.0) as client:
|
|
30
|
+
resp = client.post(f"{api_url}/auth/identify", json=payload)
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
data = resp.json()
|
|
33
|
+
|
|
34
|
+
token = data["access_token"]
|
|
35
|
+
save_session({
|
|
36
|
+
"token": token,
|
|
37
|
+
"actor_id": actor_id,
|
|
38
|
+
"actor_type": actor_type,
|
|
39
|
+
"role": role,
|
|
40
|
+
"api_url": api_url,
|
|
41
|
+
})
|
|
42
|
+
console.print(f"[bold green]✓[/bold green] Authenticated as [cyan]{actor_id}[/cyan] [{actor_type}]")
|
|
43
|
+
except httpx.ConnectError:
|
|
44
|
+
console.print(f"[bold red]✗ Failed to connect to AK5 Gateway at {api_url}[/bold red]")
|
|
45
|
+
console.print("Make sure the backend server is running: [yellow]uvicorn ak5.main:app[/yellow]")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[bold red]✗ Login failed:[/bold red] {e}")
|
ak5/cli/commands/mcp.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import uvicorn
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command("serve")
|
|
9
|
+
@click.option("--host", default="127.0.0.1", help="Host interface to bind")
|
|
10
|
+
@click.option("--port", default=8000, type=int, help="Port to bind")
|
|
11
|
+
@click.option("--reload", is_flag=True, help="Enable auto-reload for development")
|
|
12
|
+
def serve_command(host: str, port: int, reload: bool) -> None:
|
|
13
|
+
"""Start the AK5 Gateway REST API & Embedded MCP Server."""
|
|
14
|
+
console.print(f"[bold cyan]🚀 Starting AK5 Gateway on http://{host}:{port}...[/bold cyan]")
|
|
15
|
+
console.print(f" - REST API & Swagger: [link=http://{host}:{port}/docs]http://{host}:{port}/docs[/link]")
|
|
16
|
+
console.print(f" - Embedded MCP SSE: [link=http://{host}:{port}/mcp/sse]http://{host}:{port}/mcp/sse[/link]")
|
|
17
|
+
console.print(f" - SSE Real-time Bus: [link=http://{host}:{port}/api/v1/events/stream]http://{host}:{port}/api/v1/events/stream[/link]\n")
|
|
18
|
+
uvicorn.run("ak5.main:app", host=host, port=port, reload=reload)
|
ak5/cli/config.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
SESSION_FILE = Path.home() / ".ak5_session.json"
|
|
6
|
+
DEFAULT_API_URL = "http://127.0.0.1:8000/api/v1"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_session() -> dict[str, Any]:
|
|
10
|
+
if SESSION_FILE.exists():
|
|
11
|
+
try:
|
|
12
|
+
with open(SESSION_FILE, encoding="utf-8") as f:
|
|
13
|
+
return json.load(f)
|
|
14
|
+
except (json.JSONDecodeError, OSError):
|
|
15
|
+
return {}
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_session(data: dict[str, Any]) -> None:
|
|
20
|
+
session = load_session()
|
|
21
|
+
session.update(data)
|
|
22
|
+
with open(SESSION_FILE, "w", encoding="utf-8") as f:
|
|
23
|
+
json.dump(session, f, indent=2)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_token() -> str | None:
|
|
27
|
+
return load_session().get("token")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_actor_id() -> str | None:
|
|
31
|
+
return load_session().get("actor_id")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_api_url() -> str:
|
|
35
|
+
return load_session().get("api_url", DEFAULT_API_URL)
|
ak5/cli/main.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from ak5.cli.commands.agents import agents_command
|
|
5
|
+
from ak5.cli.commands.board import board_command
|
|
6
|
+
from ak5.cli.commands.delegate import delegate_command
|
|
7
|
+
from ak5.cli.commands.demo import demo_command
|
|
8
|
+
from ak5.cli.commands.login import login_command
|
|
9
|
+
from ak5.cli.commands.mcp import mcp_command
|
|
10
|
+
from ak5.cli.commands.serve import serve_command
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.version_option(version="1.0.0", prog_name="ak5")
|
|
17
|
+
def cli():
|
|
18
|
+
"""AK5 (Agent K5) - Agent-Orchestrated Kanban Command Line Interface."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
cli.add_command(serve_command)
|
|
22
|
+
cli.add_command(mcp_command)
|
|
23
|
+
cli.add_command(board_command)
|
|
24
|
+
cli.add_command(agents_command)
|
|
25
|
+
cli.add_command(delegate_command)
|
|
26
|
+
cli.add_command(demo_command)
|
|
27
|
+
cli.add_command(login_command)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
cli()
|
ak5/config.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
PROJECT_NAME: str = "AK5"
|
|
6
|
+
VERSION: str = "1.0.0"
|
|
7
|
+
API_V1_STR: str = "/api/v1"
|
|
8
|
+
|
|
9
|
+
DATABASE_URL: str = "sqlite+aiosqlite:///./ak5.db"
|
|
10
|
+
SQLITE_BUSY_TIMEOUT: int = 5000
|
|
11
|
+
|
|
12
|
+
JWT_SECRET: str = "ak5-dev-secret-key-change-in-production-2026"
|
|
13
|
+
JWT_ALGORITHM: str = "HS256"
|
|
14
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days
|
|
15
|
+
|
|
16
|
+
DEFAULT_BOARD_ID: str = "proj-core-engine"
|
|
17
|
+
DEFAULT_BOARD_NAME: str = "Core Engine Project"
|
|
18
|
+
|
|
19
|
+
CORS_ORIGINS: list[str] = [
|
|
20
|
+
"http://localhost:3000",
|
|
21
|
+
"http://127.0.0.1:3000",
|
|
22
|
+
"http://localhost:8000",
|
|
23
|
+
"http://127.0.0.1:8000",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
settings = Settings()
|