feat: add NORTH_STAR.md and MVP core loop (Telegram bot + LLM + proposal generator)

This commit is contained in:
copilot-swe-agent[bot]
2026-07-25 11:47:52 +00:00
committed by GitHub
parent c071fe6bf7
commit 6862c7d0b8
17 changed files with 1392 additions and 0 deletions
View File
+106
View File
@@ -0,0 +1,106 @@
"""Proposal data model and generator.
This module implements a minimal "generate proposal" workflow:
1. Fetch data from a target API endpoint.
2. Ask the LLM to analyse the data and produce a structured proposal.
3. Return the proposal for delivery (e.g. via Telegram).
The Proposal dataclass is intentionally simple for the MVP – it captures the
fields described in the manifesto (why, confidence, evidence, expected outcome,
rollback strategy) without any persistence layer yet.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
import httpx
from steward.config import Settings
from steward.llm.client import LLMClient
logger = logging.getLogger(__name__)
_ANALYSIS_SYSTEM_PROMPT = (
"You are Steward, an AI operations platform. "
"You have been given raw data from an internal API. "
"Analyse the data and produce a concise proposal in the following format:\n\n"
"**Summary:** <one-sentence summary>\n"
"**Why:** <reason this proposal matters>\n"
"**Evidence:** <key data points from the API response>\n"
"**Expected outcome:** <what will improve if adopted>\n"
"**Rollback strategy:** <how to undo if things go wrong>\n"
"**Confidence:** <Low | Medium | High> - <brief justification>\n\n"
"Be conservative. If the data is healthy and no action is needed, say so explicitly."
)
@dataclass
class Proposal:
"""A structured action proposal generated by Steward."""
title: str
body: str
source_url: str
generated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
raw_data: str = ""
def format_for_telegram(self) -> str:
"""Return a Markdown-formatted string suitable for a Telegram message."""
ts = self.generated_at.strftime("%Y-%m-%d %H:%M UTC")
return (
f"\U0001f50d *Steward Proposal*\n"
f"_{ts}_\n\n"
f"*Source:* `{self.source_url}`\n\n"
f"{self.body}"
)
class ProposalGenerator:
"""Fetches data from a target API and generates a proposal via the LLM."""
def __init__(self, settings: Settings, llm: LLMClient) -> None:
self._settings = settings
self._llm = llm
async def run(self) -> Proposal | None:
"""Fetch the target API and return a Proposal, or None on error."""
url = self._settings.analysis_target_url
if not url:
logger.warning("analysis_target_url is not configured - skipping proposal generation")
return None
raw = await self._fetch(url)
if raw is None:
return None
body = await self._llm.chat(
f"Here is the API response from {url}:\n\n{raw}",
system_prompt=_ANALYSIS_SYSTEM_PROMPT,
)
return Proposal(
title="Daily Analysis",
body=body,
source_url=url,
raw_data=raw,
)
async def _fetch(self, url: str) -> str | None:
"""Fetch the URL and return the response body as text."""
headers: dict[str, str] = {}
api_key = self._settings.analysis_target_api_key
if api_key:
headers["Authorization"] = "Bearer " + api_key
try:
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
return response.text
except httpx.HTTPError as exc:
logger.error("Failed to fetch %s: %s", url, exc)
return None