fix(stats): restore dynamic badges and update community statistics
- Recover dynamic Shields.io badges in README by restoring missing Gist ID 'db3d95687075a880af6f1fba76d679c6'. - Add 'tool' to DOWNLOADABLE_TYPES and implement TYPE_ALIASES for normalization (mapping 'tools' to 'tool'). - Update community statistics and Ranking list (Top 6) based on latest marketplace data. - Refactor openwebui_stats.py with 100% English comments and enhanced user ID auto-resolution. - Verify Smart Mind Map (#1) and other top plugins maintain correct sorting.
This commit is contained in:
447
plugins/debug/copilot-sdk/auto_programming_task.py
Normal file
447
plugins/debug/copilot-sdk/auto_programming_task.py
Normal file
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run an autonomous programming task via Copilot SDK.
|
||||
|
||||
Usage:
|
||||
python plugins/debug/copilot-sdk/auto_programming_task.py \
|
||||
--task "Fix failing tests in tests/test_xxx.py" \
|
||||
--cwd /Users/fujie/app/python/oui/openwebui-extensions
|
||||
|
||||
Notes:
|
||||
- Default model is gpt-5-mini (low-cost for repeated runs).
|
||||
- This script DOES NOT pin/upgrade SDK versions.
|
||||
- Copilot CLI must be available (or set COPILOT_CLI_PATH).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
DEFAULT_TASK = (
|
||||
"Convert plugins/actions/smart-mind-map/smart_mind_map.py (Action plugin) "
|
||||
"into a Tool plugin implementation under plugins/tools/. "
|
||||
"Keep Copilot SDK version unchanged, follow patterns from "
|
||||
"plugins/pipes/github-copilot-sdk/, and implement a runnable MVP with "
|
||||
"i18n/status events/basic validation."
|
||||
)
|
||||
|
||||
|
||||
def _ensure_copilot_importable() -> None:
|
||||
"""Try local SDK path fallback if `copilot` package is not installed."""
|
||||
try:
|
||||
import copilot # noqa: F401
|
||||
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
candidates = []
|
||||
|
||||
env_path = os.environ.get("COPILOT_SDK_PYTHON_PATH", "").strip()
|
||||
if env_path:
|
||||
candidates.append(Path(env_path))
|
||||
|
||||
# Default sibling repo path: ../copilot-sdk/python
|
||||
# Current file: plugins/debug/copilot-sdk/auto_programming_task.py
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
candidates.append(repo_root.parent / "copilot-sdk" / "python")
|
||||
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
sys.path.insert(0, str(path))
|
||||
try:
|
||||
import copilot # noqa: F401
|
||||
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
raise RuntimeError(
|
||||
"Cannot import `copilot` package. Install copilot-sdk python package "
|
||||
"or set COPILOT_SDK_PYTHON_PATH to copilot-sdk/python directory."
|
||||
)
|
||||
|
||||
|
||||
def _build_agent_prompt(task: str, cwd: str, extra_context: Optional[str]) -> str:
|
||||
extra = extra_context.strip() if extra_context else ""
|
||||
return textwrap.dedent(
|
||||
f"""
|
||||
You are an autonomous coding agent running in workspace: {cwd}
|
||||
|
||||
Primary task:
|
||||
{task}
|
||||
|
||||
Requirements:
|
||||
1. Inspect relevant files and implement changes directly in the workspace.
|
||||
2. Keep changes minimal and focused on the task.
|
||||
3. If tests/build commands exist, run targeted validation for changed scope.
|
||||
4. If blocked, explain the blocker and propose concrete next steps.
|
||||
5. At the end, provide a concise summary of:
|
||||
- files changed
|
||||
- what was implemented
|
||||
- validation results
|
||||
|
||||
{f'Additional context:\n{extra}' if extra else ''}
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
def _build_planning_prompt(task: str, cwd: str, extra_context: Optional[str]) -> str:
|
||||
extra = extra_context.strip() if extra_context else ""
|
||||
return textwrap.dedent(
|
||||
f"""
|
||||
You are a senior autonomous coding planner running in workspace: {cwd}
|
||||
|
||||
User requirement (may be underspecified):
|
||||
{task}
|
||||
|
||||
Goal:
|
||||
Expand the requirement into an actionable implementation plan that can be executed end-to-end without extra clarification whenever possible.
|
||||
|
||||
Output format (strict):
|
||||
1) Expanded Objective (clear, concrete, scoped)
|
||||
2) Assumptions (only necessary assumptions)
|
||||
3) Step-by-step Plan (ordered, verifiable)
|
||||
4) Validation Plan (how to verify changes)
|
||||
5) Execution Brief (concise instruction for implementation agent)
|
||||
|
||||
Constraints:
|
||||
- Keep scope minimal and aligned with the user requirement.
|
||||
- Do not invent unrelated features.
|
||||
- Prefer practical MVP completion.
|
||||
|
||||
{f'Additional context:\n{extra}' if extra else ''}
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
def _build_execution_prompt(
|
||||
task: str, cwd: str, extra_context: Optional[str], plan_text: str
|
||||
) -> str:
|
||||
extra = extra_context.strip() if extra_context else ""
|
||||
return textwrap.dedent(
|
||||
f"""
|
||||
You are an autonomous coding agent running in workspace: {cwd}
|
||||
|
||||
User requirement:
|
||||
{task}
|
||||
|
||||
Planner output (must follow):
|
||||
{plan_text}
|
||||
|
||||
Execution requirements:
|
||||
1. Execute the plan directly; do not stop after analysis.
|
||||
2. If the original requirement is underspecified, use the planner assumptions and continue.
|
||||
3. Keep changes minimal, focused, and runnable.
|
||||
4. Run targeted validation for changed scope where possible.
|
||||
5. If blocked by missing prerequisites, report blocker and the smallest next action.
|
||||
6. Finish with concise summary:
|
||||
- files changed
|
||||
- implemented behavior
|
||||
- validation results
|
||||
|
||||
{f'Additional context:\n{extra}' if extra else ''}
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
async def _run_single_session(
|
||||
client,
|
||||
args: argparse.Namespace,
|
||||
prompt: str,
|
||||
stage_name: str,
|
||||
stream_output: bool,
|
||||
) -> tuple[int, str]:
|
||||
from copilot.types import PermissionHandler
|
||||
|
||||
def _auto_user_input_handler(request, _invocation):
|
||||
question = ""
|
||||
if isinstance(request, dict):
|
||||
question = str(request.get("question", "")).lower()
|
||||
choices = request.get("choices") or []
|
||||
if choices and isinstance(choices, list):
|
||||
preferred = args.auto_user_answer.strip()
|
||||
for choice in choices:
|
||||
c = str(choice)
|
||||
if preferred and preferred.lower() == c.lower():
|
||||
return {"answer": c, "wasFreeform": False}
|
||||
return {"answer": str(choices[0]), "wasFreeform": False}
|
||||
|
||||
preferred = args.auto_user_answer.strip() or "continue"
|
||||
if "confirm" in question or "proceed" in question:
|
||||
preferred = "yes"
|
||||
return {"answer": preferred, "wasFreeform": True}
|
||||
|
||||
session_config = {
|
||||
"model": args.model,
|
||||
"reasoning_effort": args.reasoning_effort,
|
||||
"streaming": True,
|
||||
"infinite_sessions": {
|
||||
"enabled": True,
|
||||
},
|
||||
"on_permission_request": PermissionHandler.approve_all,
|
||||
"on_user_input_request": _auto_user_input_handler,
|
||||
}
|
||||
|
||||
session = await client.create_session(session_config)
|
||||
|
||||
done = asyncio.Event()
|
||||
full_messages = []
|
||||
has_error = False
|
||||
|
||||
def on_event(event):
|
||||
nonlocal has_error
|
||||
etype = getattr(event, "type", "unknown")
|
||||
if hasattr(etype, "value"):
|
||||
etype = etype.value
|
||||
|
||||
if args.trace_events:
|
||||
print(f"\n[{stage_name}][EVENT] {etype}", flush=True)
|
||||
|
||||
if etype == "assistant.message_delta" and stream_output:
|
||||
delta = getattr(event.data, "delta_content", "") or ""
|
||||
if delta:
|
||||
print(delta, end="", flush=True)
|
||||
|
||||
elif etype == "assistant.message":
|
||||
content = getattr(event.data, "content", "") or ""
|
||||
if content:
|
||||
full_messages.append(content)
|
||||
|
||||
elif etype == "session.error":
|
||||
has_error = True
|
||||
done.set()
|
||||
elif etype == "session.idle":
|
||||
done.set()
|
||||
|
||||
unsubscribe = session.on(on_event)
|
||||
heartbeat_task = None
|
||||
|
||||
async def _heartbeat():
|
||||
while not done.is_set():
|
||||
await asyncio.sleep(max(3, int(args.heartbeat_seconds)))
|
||||
if not done.is_set():
|
||||
print(
|
||||
f"[{stage_name}][heartbeat] waiting for assistant events...",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
try:
|
||||
heartbeat_task = asyncio.create_task(_heartbeat())
|
||||
await session.send({"prompt": prompt, "mode": "immediate"})
|
||||
await asyncio.wait_for(done.wait(), timeout=args.timeout)
|
||||
|
||||
if stream_output:
|
||||
print("\n")
|
||||
|
||||
final_message = full_messages[-1] if full_messages else ""
|
||||
if final_message:
|
||||
print(f"\n===== {stage_name} FINAL MESSAGE =====\n")
|
||||
print(final_message)
|
||||
|
||||
if has_error:
|
||||
return 1, final_message
|
||||
return 0, final_message
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f"\n❌ [{stage_name}] Timeout after {args.timeout}s")
|
||||
return 124, ""
|
||||
except Exception as exc:
|
||||
print(f"\n❌ [{stage_name}] Run failed: {exc}")
|
||||
return 1, ""
|
||||
finally:
|
||||
if heartbeat_task:
|
||||
heartbeat_task.cancel()
|
||||
try:
|
||||
unsubscribe()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await session.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def run_task(args: argparse.Namespace) -> int:
|
||||
_ensure_copilot_importable()
|
||||
|
||||
from copilot import CopilotClient
|
||||
|
||||
task_text = (args.task or "").strip()
|
||||
if args.task_file:
|
||||
task_text = Path(args.task_file).read_text(encoding="utf-8").strip()
|
||||
|
||||
if not task_text:
|
||||
task_text = DEFAULT_TASK
|
||||
|
||||
direct_prompt = _build_agent_prompt(task_text, args.cwd, args.extra_context)
|
||||
|
||||
client_options = {
|
||||
"cwd": args.cwd,
|
||||
"log_level": args.log_level,
|
||||
}
|
||||
|
||||
if args.cli_path:
|
||||
client_options["cli_path"] = args.cli_path
|
||||
|
||||
if args.github_token:
|
||||
client_options["github_token"] = args.github_token
|
||||
|
||||
print(f"🚀 Starting Copilot SDK task runner")
|
||||
print(f" cwd: {args.cwd}")
|
||||
print(f" model: {args.model}")
|
||||
print(f" reasoning_effort: {args.reasoning_effort}")
|
||||
print(f" plan_first: {args.plan_first}")
|
||||
|
||||
client = CopilotClient(client_options)
|
||||
await client.start()
|
||||
|
||||
try:
|
||||
if args.plan_first:
|
||||
planning_prompt = _build_planning_prompt(
|
||||
task_text, args.cwd, args.extra_context
|
||||
)
|
||||
print("\n🧭 Stage 1/2: Planning and requirement expansion")
|
||||
plan_code, plan_text = await _run_single_session(
|
||||
client=client,
|
||||
args=args,
|
||||
prompt=planning_prompt,
|
||||
stage_name="PLANNING",
|
||||
stream_output=False,
|
||||
)
|
||||
if plan_code != 0:
|
||||
return plan_code
|
||||
|
||||
execution_prompt = _build_execution_prompt(
|
||||
task=task_text,
|
||||
cwd=args.cwd,
|
||||
extra_context=args.extra_context,
|
||||
plan_text=plan_text or "(No planner output provided)",
|
||||
)
|
||||
print("\n⚙️ Stage 2/2: Execute plan autonomously")
|
||||
exec_code, _ = await _run_single_session(
|
||||
client=client,
|
||||
args=args,
|
||||
prompt=execution_prompt,
|
||||
stage_name="EXECUTION",
|
||||
stream_output=args.stream,
|
||||
)
|
||||
return exec_code
|
||||
|
||||
print("\n⚙️ Direct mode: Execute task without planning stage")
|
||||
exec_code, _ = await _run_single_session(
|
||||
client=client,
|
||||
args=args,
|
||||
prompt=direct_prompt,
|
||||
stage_name="EXECUTION",
|
||||
stream_output=args.stream,
|
||||
)
|
||||
return exec_code
|
||||
finally:
|
||||
try:
|
||||
await client.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run one autonomous programming task with Copilot SDK"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--task",
|
||||
default="",
|
||||
help="Task description text (if empty, uses built-in default task)",
|
||||
)
|
||||
parser.add_argument("--task-file", default="", help="Path to a task text file")
|
||||
parser.add_argument("--cwd", default=os.getcwd(), help="Workspace directory")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="gpt-5-mini",
|
||||
help="Model id for Copilot session (default: gpt-5-mini)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reasoning-effort",
|
||||
default="medium",
|
||||
choices=["low", "medium", "high", "xhigh"],
|
||||
help="Reasoning effort",
|
||||
)
|
||||
parser.add_argument("--timeout", type=int, default=1800, help="Timeout seconds")
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
default="info",
|
||||
choices=["trace", "debug", "info", "warn", "error"],
|
||||
help="Copilot client log level",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--github-token",
|
||||
default=os.environ.get("GH_TOKEN", ""),
|
||||
help="Optional GitHub token; defaults to GH_TOKEN",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cli-path",
|
||||
default=os.environ.get("COPILOT_CLI_PATH", ""),
|
||||
help="Optional Copilot CLI path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extra-context",
|
||||
default="",
|
||||
help="Optional extra context appended to the task prompt",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stream",
|
||||
action="store_true",
|
||||
help="Print assistant delta stream in real-time",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--trace-events",
|
||||
action="store_true",
|
||||
help="Print each SDK event type for debugging",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auto-user-answer",
|
||||
default="continue",
|
||||
help="Default answer for on_user_input_request",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--heartbeat-seconds",
|
||||
type=int,
|
||||
default=12,
|
||||
help="Heartbeat interval while waiting for events",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plan-first",
|
||||
action="store_true",
|
||||
help="Run planning stage before execution (default behavior)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-plan-first",
|
||||
action="store_true",
|
||||
help="Disable planning stage and run direct execution",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.task_file and (args.task or "").strip():
|
||||
parser.error("Use either --task or --task-file, not both")
|
||||
|
||||
args.plan_first = True
|
||||
if args.no_plan_first:
|
||||
args.plan_first = False
|
||||
elif args.plan_first:
|
||||
args.plan_first = True
|
||||
|
||||
return asyncio.run(run_task(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user