feat: add NORTH_STAR.md and MVP core loop (Telegram bot + LLM + proposal generator)
This commit is contained in:
committed by
GitHub
parent
c071fe6bf7
commit
6862c7d0b8
@@ -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
|
||||
Reference in New Issue
Block a user