diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000000..82f8dbd960 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,70 @@ +# This workflow will upload a Python Package to PyPI when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + release-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build release distributions + run: | + # NOTE: put your own distribution build steps here. + python -m pip install build + python -m build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: + runs-on: ubuntu-latest + needs: + - release-build + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + # Dedicated environments with protections for publishing are strongly recommended. + # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules + environment: + name: pypi + # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: + # url: https://pypi.org/p/YOURPROJECT + # + # ALTERNATIVE: if your GitHub Release name is the PyPI project version string + # ALTERNATIVE: exactly, uncomment the following line instead: + # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ diff --git a/docs/README.md b/docs/README.md index 40ca2ff999..41e56d8a49 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,7 @@ To begin with Agent Zero, follow the links below for detailed guides on various - **[Installation](installation.md):** Set up (or [update](installation.md#how-to-update-agent-zero)) Agent Zero on your system. - **[Usage Guide](usage.md):** Explore GUI features and usage scenarios. - **[Architecture Overview](architecture.md):** Understand the internal workings of the framework. +- **[Token Compression Protocol](token_compression_protocol.md):** Run the TCP service and integrate it with clients (including browser extensions). - **[Contributing](contribution.md):** Learn how to contribute to the Agent Zero project. - **[Troubleshooting and FAQ](troubleshooting.md):** Find answers to common issues and questions. @@ -59,6 +60,7 @@ To begin with Agent Zero, follow the links below for detailed guides on various - [Making Changes](contribution.md#making-changes) - [Submitting a Pull Request](contribution.md#submitting-a-pull-request) - [Documentation Stack](contribution.md#documentation-stack) +- [Token Compression Protocol](token_compression_protocol.md) - [Troubleshooting and FAQ](troubleshooting.md) - [Frequently Asked Questions](troubleshooting.md#frequently-asked-questions) - [Troubleshooting](troubleshooting.md#troubleshooting) \ No newline at end of file diff --git a/docs/autonomous_listing_service.md b/docs/autonomous_listing_service.md new file mode 100644 index 0000000000..b0478c97f7 --- /dev/null +++ b/docs/autonomous_listing_service.md @@ -0,0 +1,186 @@ +# Autonomous Listing Service – Technical Blueprint + +This document describes the design of an AI-native service that turns a seller’s raw photos and notes into premium listings, syndicates them across major marketplaces (Craigslist, Mercari, Nextdoor, etc.), and provides a unified conversational interface for negotiating with buyers. The system is intentionally agentic, multi-LLM, and RAG-enabled while running on a lightweight Python container suitable for serverless deployments. + +--- + +## 1. Goals & Constraints +- **Delightful Listings:** Transform mediocre images + short descriptions into polished media and persuasive narratives that boost conversions. +- **One-Click Syndication:** Publish consistently formatted listings to multiple platforms with per-channel compliance. +- **Unified Messaging:** Give sellers a Zoom-like conversational hub to coordinate with AI agents and buyers while listings are live. +- **Autonomous Lifecycle:** Monitor inquiries, negotiate within guardrails, and auto-close listings once an item sells. +- **Portable Runtime:** Deliver as a Python-first, containerized micro-app deployable on Lambda, Cloud Run, or Fargate. + +--- + +## 2. High-Level Architecture + +``` +User → Web/App UI → API Gateway → Python Orchestrator (FastAPI) → Agent Mesh + ↘ Event Bus / Task Queue + ↘ Worker Pods (image, LLM, RAG, integrators) +Storage Layers: Object storage (images), Vector DB (descriptions/market data), Relational DB (listings, chats) +``` + +### Core Services +1. **Ingestion & Auth** + - Accepts photo uploads, metadata, and voice/text notes. + - Performs safe content checks before processing. +2. **AI Creativity Pipeline** + - Image Enhancement Agent: Upscales, denoises, applies lighting corrections, and composes collage thumbnails. + - Styling Agent: Suggests background removal or contextual scenes (e.g., staging furniture). + - Narrative Agent: Generates long-form descriptions leveraging a marketing RAG corpus + sentiment tuning for each marketplace. +3. **Marketplace Integrators** + - Channel-specific adapters for Craigslist, Mercari, Nextdoor, (extensible to Facebook Marketplace, OfferUp, etc.). + - Normalizes categories, pricing, shipping, and handles platform-specific throttling/anti-bot rules. +4. **Engagement Hub** + - Real-time messaging service bridging buyers (email/SMS/in-platform chat) with seller + negotiation agents. + - Shared timeline UI showing offers, counteroffers, and status changes. +5. **Lifecycle Controller** + - Tracks listing states (draft → scheduled → live → pending sale → closed). + - Automatically unlists from all channels once a purchase is confirmed. + +--- + +## 3. Agentic Workflow + +| Step | Agent | Description | LLM Model(s) | +| --- | --- | --- | --- | +| Intake | Concierge Agent | Confirms item details, requests missing info, runs safety checklist. | GPT-4o or Claude 3.5 Sonnet | +| Visual Polish | Vision Stylist & Enhancer | Applies upscaling, background cleanup, style transfer tuned per category. | Stable Diffusion XL / ControlNet + DeepSeek-VL for QA | +| Narrative Crafting | Listing Copywriter | Generates short + long descriptions, bullet highlights, SEO tags, shipping guidance. Uses RAG on market best practices. | GPT-4.1 / Gemini 1.5 Pro | +| Valuation | Pricing Analyst | Benchmarks with comps fetched via search APIs; suggests optimal price tiers. | Claude 3.5 Haiku + internal comps DB | +| Syndication | Channel Publisher | Maps listing to each platform’s schema, posts, and verifies success. | Tool-executing agent via FastAPI + Selenium/API | +| Engagement | Buyer Liaison | Monitors inquiries, drafts responses, escalates to seller when negotiation boundaries hit. | GPT-4o mini (fast) with guardrails | +| Closure | Lifecycle Steward | Detects sale confirmation, auto-closes all channels, generates pick-up instructions. | Rule-based + LLM verification | + +Agents communicate via the existing `call_subordinate` + `knowledge_tool` primitives, storing context in `memory`/`knowledge` for reuse (e.g., pricing heuristics, category guidelines). + +--- + +## 4. AI Pipeline Details + +### 4.1 Image Enhancement +- **Stages:** (1) quality assessment → (2) super-resolution (e.g., Real-ESRGAN) → (3) background cleanup (Matte-ing) → (4) lighting & color grading → (5) layout collage. +- **Outputs:** hero image, 3–5 gallery shots, detail zooms, and optional lifestyle composite scene. +- **Instrumentation:** Each step logs metrics (sharpness delta, noise reduction) for future fine-tuning. + +### 4.2 Description + Sentiment Crafting +- **Inputs:** Seller notes, extracted metadata (dimensions via OCR, brand logos, etc.), prior sales comps. +- **RAG Sources:** Marketing playbooks, brand tone guides, compliance docs per platform. +- **Outputs:** + - Title optimized for SEO, + - Rich paragraph + bullet list, + - Condition disclosures, + - Suggested hashtags and shipping/pickup text. +- **Tone Tuning:** Different prompt templates per platform (e.g., concise for Craigslist, lifestyle-forward for Nextdoor). + +### 4.3 Pricing & Strategy +- Pulls live comps via aggregator APIs (where permitted) or stored datasets. +- Generates price ladder (list price, “fast-sale” price, minimum acceptable). +- Feeds guardrails to Buyer Liaison (auto-approve offers above threshold, escalate otherwise). + +--- + +## 5. Marketplace Integrations + +| Platform | Integration Mode | Notes | +| --- | --- | --- | +| Craigslist | Headless browser automation (Playwright) + email relay for replies. | Needs CAPTCHA-solving strategy (vision model + manual fallback). | +| Mercari | Official API (if available) or mobile-app automation. | Supports shipping label creation; track order IDs. | +| Nextdoor | Web automation w/ community selection. | Monitor community guidelines to avoid spam flags. | +| Custom / Others | Modular adapters via interface `MarketplacePublisher`. | Easy to add OfferUp, eBay, Etsy later. | + +Failure handling: retries with exponential backoff, webhook-like callbacks to update listing state, and anomaly logging for manual review. + +--- + +## 6. Unified Seller Interface + +### UX Tenets +- **Organic Flow:** Minimal forms; conversational onboarding with dynamic checklists. +- **Zoom-like Collab:** Agents appear as avatars, announce actions (e.g., “Copywriter drafting Mercari description”). Seller can join live huddles to approve or tweak content. +- **Inbox View:** Threaded conversations grouped by platform and buyer; AI-suggested replies with quick-edit controls. +- **Visual Dashboard:** Pipeline status (Processing Images → Drafting → Live on X platforms), price ladder, performance analytics. + +### Tech Stack +- **Frontend:** React/Next.js or SvelteKit with WebSockets for live updates. +- **Backend:** Python FastAPI orchestrator running inside a slim container (e.g., distroless + uvicorn). Event-driven tasks handled by Celery/Redis or AWS SQS + Lambda workers. +- **Storage:** + - S3-compatible bucket for assets, + - Postgres for listings/offers, + - Redis/WebSocket gateway for live messaging, + - Vector DB (Qdrant/Pinecone) for RAG corpora. + +--- + +## 7. Serverless / Containerized Deployment + +| Layer | Option | Notes | +| --- | --- | --- | +| API + UI | AWS Lambda (FastAPI via Mangum) or Cloud Run | Handles synchronous interactions. | +| Workers | AWS Fargate / ECS tasks or Cloud Run Jobs | For heavier image/LLM workloads. | +| Event Bus | AWS SQS + EventBridge or Pub/Sub | Decouples ingestion from processing. | +| Media Processing | AWS Lambda w/ GPU (if available) or attached GPU service | For rapid diffusion-based adjustments. | + +CI/CD builds a single container image (FastAPI + worker binaries) pushed to ECR/GCR. Infrastructure-as-code (Terraform/Pulumi) provisions queues, storage, and secrets. + +--- + +## 8. Data & Knowledge Fabric +- **Knowledge Packs:** + - Marketing best practices, + - Platform policy summaries, + - Visual staging tips per category. +- **RAG Pipeline:** + 1. Seller intent + item metadata → embed → retrieve from vector DB, + 2. Feed retrieved snippets into Copywriter prompts, + 3. Store resulting listing in `memory/solutions` for future reuse. +- **Personalization:** Seller preferences (tone, negotiation style) saved to memory and loaded automatically when they return. + +--- + +## 9. Implementation Roadmap + +1. **Foundations** + - Spin up FastAPI skeleton + auth. + - Configure storage buckets, DB, and vector store. +2. **AI Pipeline MVP** + - Integrate image enhancer (ESRGAN) + background removal. + - Build Copywriter agent with GPT-4o + RAG from initial corpus. +3. **Marketplace Adapter Framework** + - Define `MarketplacePublisher` interface. + - Implement Craigslist + Mercari to validate both automation styles. +4. **Engagement Hub** + - Real-time messaging API + UI view. + - Buyer Liaison agent with guardrails + escalation rules. +5. **Lifecycle & Automation** + - Listing state machine, auto-closing logic, unified analytics. +6. **Serverless Packaging** + - Containerize, add IaC, deploy to sandbox environment. +7. **UX Polish** + - Zoom-like collaboration room, hero dashboard, onboarding wizards. +8. **Compliance & Monitoring** + - Logging, anomaly detection, rate-limit watchdogs, content moderation pipeline. + +--- + +## 10. Future Enhancements +- **Smart Negotiation:** Reinforcement-learning agent tuned on successful deal histories. +- **Buyer Discovery:** Cross-post to social channels with auto-generated reels/stories. +- **Shipment Automation:** Integration with UPS/FedEx APIs for instant label creation. +- **Reputation Engine:** Aggregate feedback across platforms to build seller trust profiles. +- **Predictive Demand:** Recommend best posting windows and price adjustments based on market trends. + +--- + +## 11. Co-Development with the Super Agency + +- **Mission Diary:** Maintain `docs/programs/autonomous_listing/journal.md` logging every experiment, release, and KPI shift so agents can learn from prior iterations. +- **Improvement Backlog:** Share a prioritized list (`docs/programs/autonomous_listing/improvements.md`) that feeds into the agency-wide iterative protocol (see Section 15 of `autonomous_super_agency.md`). +- **Telemetry Handoff:** Telemetry Sentinel publishes listing-service-specific health reports (conversion uplift, response latency, negotiation success rate) for Portfolio Navigator to review weekly. +- **Prompt Sync:** Copywriter, Buyer Liaison, and Publisher persona prompts live under `prompts/super-agency/roles/listing_*` and reference the same change log as the broader agency to keep behavior updates auditable. +- **Feedback Routing:** Any seller or buyer feedback entering the Engagement Hub is tagged and stored in `knowledge/custom/main/listings/` so the RAG corpus continuously reflects real-world voice-of-customer insights. +- **Release Ritual:** Every deployment of the service includes a short “What changed / Metrics impacted / Next bet” record appended to the mission diary, ensuring synchronous evolution of the agency and this product line. + +This blueprint provides a detailed path to a serverless, AI-native listing concierge that delights sellers and scales across marketplaces with minimal manual effort. diff --git a/docs/autonomous_super_agency.md b/docs/autonomous_super_agency.md new file mode 100644 index 0000000000..0a64550092 --- /dev/null +++ b/docs/autonomous_super_agency.md @@ -0,0 +1,355 @@ +# Autonomous Innovation Super-Agency Blueprint + +This document describes how to configure the Agent Zero framework to run a fully autonomous research, development, and technology commercialization agency that rivals the combined capabilities of top-tier labs. It defines agent archetypes, department structures, and low-touch protocols designed to minimize human intervention while maintaining safety and governance. + +--- + +## 1. Guiding Principles +- **Mission clarity first:** Every autonomous workflow begins with explicit OKRs and guardrails encoded in prompts and behavior rules. +- **Delegation by design:** Agents are expected to break problems into sub-missions and spin up specialized subordinates whenever scope grows. +- **Evidence over opinion:** All major decisions require citations via the knowledge tool, instrumentation logs, or code artifacts. +- **Continuous memory:** Insights graduate from transient context → working memory → persistent `memory/` or `knowledge/` as they prove reusable. +- **Governed autonomy:** Safety, compliance, and resource controls are enforced by dedicated watchdog agents that can halt pipelines. + +--- + +## 2. Agent Tiers & Worker Archetypes + +| Tier | Description | Primary Agents | +| --- | --- | --- | +| **Executive Cortex** | Owns agency-wide OKRs, budgets, and risk posture. Interfaces with the human sponsor. | Apex Orchestrator (Agent 0), Portfolio Navigator, Risk & Ethics Governor | +| **Domain Studios** | Department-level strategists that translate OKRs into thematic programs. | Research Studio Director, Product Systems Director, Platform Engineering Director, Venture & GTM Director | +| **Execution Pods** | Cross-functional squads that run experiments, build prototypes, and ship releases. | Research Fellows, ML Architects, Systems Engineers, Product Synthesists, Validation Analysts | +| **Support Mesh** | Shared services covering finance, vendor ops, compliance, talent, and facilities (virtual). | Autonomy Comptroller, Procurement Pilot, Talent Steward | +| **Autonomous Infrastructure** | Toolsmiths and observability agents that maintain instruments, logs, and knowledge graphs. | Instrument Engineer, Memory Librarian, Telemetry Sentinel | + +Each archetype maps to a reusable prompt persona stored under `prompts/super-agency/agent.system.role..md` and referenced from the main system prompt. + +--- + +## 3. Department Blueprint & Agent Definitions + +### 3.1 Strategic Command (Executive Cortex) +- **Apex Orchestrator:** Runs the top-level control loop, prioritizes missions, and approves resource allocations. Lives inside `agent.system.main.role.md`. +- **Portfolio Navigator:** Scores opportunities, manages the multi-horizon roadmap, and triggers new domain studios when capacity >70%. +- **Risk & Ethics Governor:** Monitors for policy violations using extension hooks `_30_guardrails.py`, can pause or roll back runs. + +### 3.2 Research Intelligence & Emerging Science +- **Scouting Agents:** Continuously watch frontier literature using `knowledge_tool` + SearXNG, tag findings into `knowledge/custom/main`. +- **Program Architects:** Convert scouting signals into funded research programs with explicit success metrics. +- **Experiment Fabricators:** Use `python/tools/code_execution_tool` and instruments under `instruments/research_*` to run simulations, notebooks, or lab automations. + +### 3.3 Product Systems & Experience Lab +- **Product Synthesists:** Translate research outputs into user-facing narratives, JTBD documents, and requirement specs saved in `docs/roadmaps/`. +- **Experience Choreographers:** Prototype UI flows leveraging `webui/` components or low-code instruments. +- **Adoption Analysts:** Instrument telemetry dashboards to test desirability and engagement hypotheses. + +### 3.4 Engineering & Platform +- **Core Systems Engineers:** Extend `python/` services, own CI/CD, and steward technical debt registries. +- **ML/AI Engineers:** Package models, manage fine-tuning jobs, and maintain evaluation harnesses in `python/extensions/evals/`. +- **Automation Operators:** Build instruments (shell/python scripts) to interact with external infra, data lakes, or deployment targets. + +### 3.5 Growth, Ventures & Partnerships +- **Ecosystem Cartographers:** Map ecosystems, scout potential partners, and maintain opportunity graphs in knowledge base. +- **Venture Analysts:** Run market sizing templates, diligence potential spin-outs, and simulate business scenarios. +- **Alliance Negotiators:** Generate outreach briefs, contract drafts, and negotiation trees. + +### 3.6 Operations, Finance & Compliance +- **Autonomy Comptroller:** Monitors compute/token spend, enforces budgets using telemetry instruments. +- **Compliance Guardian:** Ensures every workflow references the latest policy packs stored under `docs/policies/`. +- **Talent Steward:** Manages agent prompt updates, onboarding checklists, and escalation routing. + +--- + +## 4. Core Systems Mapped to the Repo + +| Capability | Repo Anchor | Notes | +| --- | --- | --- | +| Prompt hierarchy | `prompts/default/` → `prompts/super-agency/` | Copy defaults, add persona-specific files, reference via settings. | +| Behavior rules | `behaviour/*.md` + `python/tools/behaviour_adjustment.py` | Executive Cortex updates rules at runtime for global pivots. | +| Tools & instruments | `python/tools/`, `instruments/` | Department-specific instruments encapsulate workflows without bloating prompts. | +| Memory & knowledge | `memory/`, `knowledge/`, `docs/` | Research insights and SOPs persist for reuse and grounding. | +| Extensions | `python/extensions/` | Add watchdog, telemetry, and summarization hooks to enforce autonomy constraints. | +| Web UI + CLI | `webui/`, `run_cli.py`, Docker stack | Enables monitoring dashboards and manual overrides if needed. | + +--- + +## 5. Low-Touch Protocols & Workflows + +### 5.1 Mission Intake & Prioritization +1. Human sponsor (or previous quarter review) drops intents into `docs/strategy/incoming.md`. +2. Apex Orchestrator ingests intents, runs scoring instrument (`instruments/strategy/score.sh`), and updates `behaviour.md` with fresh OKRs. +3. Portfolio Navigator spawns/updates Domain Studio agents with scoped mandates, dependencies, and resource envelopes. + +### 5.2 Research Sprint Loop +1. Scouting Agents continuously run watchlists (scheduled via cron + CLI) and push summaries to knowledge base. +2. Program Architects cluster findings, design hypotheses, and enqueue experiments in `docs/research_backlog.md`. +3. Experiment Fabricators pull tasks, execute code/instruments, and auto-upload artifacts to `logs/` + `memory/solutions`. +4. Risk Governor samples results, checking for compliance or reproducibility flags before promotion. + +### 5.3 Concept-to-Product Pipeline +1. Product Synthesist consumes validated research outputs and drafts Product Requirement Packs (PRPs). +2. Experience Choreographers auto-generate UX prototypes or narrative demos via `webui` tooling. +3. Core Systems Engineers size effort, generate milestones, and spawn Execution Pods per component. +4. Adoption Analysts configure telemetry instruments and success metrics before code freeze. + +### 5.4 Build, Test, and Deployment Workflow +1. Execution Pods operate under Agile-by-default prompts: plan → implement → test via built-in code execution and unit suites. +2. Telemetry Sentinel extensions capture runtime logs, feeding Observability dashboards accessible via the Web UI. +3. Deployment scripts (Docker, CI/CD) run through automation operators; only Compliance Guardian or Apex Orchestrator can halt or approve final releases. + +### 5.5 Knowledge Capture & Memory Hygiene +1. Every completed task triggers a `memory_tool` write: summary, decision, key artifacts. +2. Librarian agents promote recurring assets into `knowledge/custom/main` with embeddings. +3. Weekly automated maintenance jobs deduplicate, compress, or archive memories using `memory_tool forget` flows. + +### 5.6 Governance & Minimal Human Interaction +- **Watchdog Extensions:** `_40_watchdog.py` evaluates tool outputs, halting loops on anomaly scores. +- **Budget Fuses:** Autonomy Comptroller reads telemetry instruments and updates behavior rules if spend > thresholds. +- **Compliance Hooks:** Policies stored in `docs/policies/` are injected into prompts for any workflow touching regulated domains. +- **Escalation Matrix:** Only Apex Orchestrator pings the human sponsor, and only when blockers exceed pre-defined severity. + +--- + +## 6. Implementation Roadmap Inside Agent Zero + +1. **Create Prompt Set:** Duplicate `prompts/default` → `prompts/super-agency`, customize role files per persona, update `agent.system.main.md` to reference the new set. +2. **Encode Departments:** For each department, define: + - Role description snippet + - Delegation heuristics (when to spawn sub-agents/instruments) + - Required tools/instruments list +3. **Build Instruments:** Scaffold scripts under `instruments//` for scoring, experiment automation, budgeting, telemetry, and knowledge ops. +4. **Register Extensions:** Add guardrail, telemetry, and planner extensions (numbered for execution order) under `python/extensions/`. +5. **Seed Knowledge:** Populate `knowledge/custom/main` with policy docs, partner intel, research taxonomies, and SOPs. +6. **Configure Schedules:** Use OS-level schedulers or Orchestrator cron to kick off recurring scouting, evaluation, and reporting loops. +7. **Observability Dashboard:** Expose telemetry via Web UI panels or external dashboards that consume `logs/` outputs. + +--- + +## 7. Minimal Human Touchpoints +- **Quarterly OKR Refresh:** Sponsor reviews the Apex Orchestrator’s plan and adjusts funding envelopes. +- **Exception Handling:** Humans intervene only when watchdog agents escalate (safety, regulatory, catastrophic failure). +- **Validation Spot-Checks:** Optional human audits sample 5–10% of major releases for assurance. + +By codifying the above structure inside Agent Zero’s prompt, memory, and instrument layers, the organization operates as a cohesive, autonomous innovation powerhouse with humans positioned purely as goal setters and safety reviewers. + +--- + +## 8. Prompt Persona Template Library + +### 8.1 Repository Layout +``` +prompts/ + super-agency/ + agent.system.main.md + agent.system.main.role.md + agent.system.role.apex_orchestrator.md + agent.system.role.portfolio_navigator.md + agent.system.role.risk_governor.md + agent.system.role.research_fellow.md + ... +``` +- Duplicate `prompts/default/*` into `prompts/super-agency/*` as a baseline. +- Update `settings.yml` (or Web UI Agent Config) to point at the new subdirectory. + +### 8.2 Persona Snippet Template +Store each persona file as a reusable fragment referenced from `agent.system.main.role.md`. + +``` +### Persona: Apex Orchestrator +- Mission: translate sponsor intent into prioritized, budgeted missions for the entire agency. +- Delegation Rules: spawn Domain Studio Director when task spans >1 discipline; delegate to Support Mesh when budget/compliance implications arise. +- Mandatory Tools: knowledge_tool (for market context), behavior_adjustment (for OKR updates), call_subordinate. +- Required Outputs: OKR table, dependency map, escalation log. +- Guardrails: never commit resources without citing telemetry or research artifacts; always log plan revisions to memory. +``` + +### 8.3 Prompt Injection Hooks +- `agent.system.main.md`: reference each persona file using `{{file:prompts/super-agency/agent.system.role..md}}`. +- `agent.system.tools.md`: include only tools relevant to the persona set to minimize token load. +- `behaviour.merge.msg.md`: keep persona-specific behavior overrides short; complex procedures should live in instruments. + +--- + +## 9. Cross-Domain Workflow Protocols + +### 9.1 Mission Orchestration Handshake +1. Apex Orchestrator receives new intent and runs the Scoring Instrument. +2. Portfolio Navigator compares active capacity vs. forecast and either: + - Reprioritizes current missions, or + - Spins up a new Domain Studio agent with a scoped charter. +3. Risk & Ethics Governor is pinged asynchronously with the proposed plan to attach mandatory compliance packets before work begins. + +### 9.2 Research-to-Build Relay +1. Research Studio Director finalizes experiment bundle and pushes it to `docs/research_exports//`. +2. Product Synthesist consumes bundle, drafts PRP, and logs it to memory with tag `prp:`. +3. Platform Engineering Director auto-generates work packages, ensuring each includes: + - Linked artifacts (code, datasets) + - Tool requirements + - Acceptance tests and telemetry hooks +4. Execution Pod Agents pull work packages FIFO and report progress via subordinate agents or memory updates. + +### 9.3 Feedback & Continuous Learning Loop +1. Telemetry Sentinel aggregates KPIs per mission and saves dashboards into `logs/dashboards/.html`. +2. Adoption Analysts and Venture Analysts subscribe to the same telemetry feed to derive user/market insights. +3. Portfolio Navigator evaluates KPI deltas weekly; if metrics fall below thresholds, it automatically issues a `behaviour_adjustment` request that tightens resource use or revises OKRs. +4. Librarian Agents promote new learnings into the knowledge base, tagging them with mission IDs and taxonomy labels for rapid retrieval. + +### 9.4 Safety Net Protocol +- Guardrail extensions monitor: + - Tool output sentiment / toxicity + - Resource spikes + - Compliance keyword hits +- On anomaly detection, `call_superior` is triggered with a structured report, pausing subordinate agents until Apex Orchestrator clears the incident or escalates to a human sponsor. + +--- + +## 10. Instrument & Extension Scaffold Checklist + +| Component | Location | Purpose | +| --- | --- | --- | +| `score.sh` | `instruments/strategy/` | Multi-factor opportunity scoring (impact, effort, risk). | +| `budget_guard.py` | `python/extensions/` (e.g., `_35_budget_guard.py`) | Monitors token/compute usage; triggers behavior adjustments on overruns. | +| `watchdog.py` | `python/extensions/` (e.g., `_40_watchdog.py`) | Validates outputs, halts workflows on anomalies. | +| `telemetry_push.sh` | `instruments/ops/` | Publishes mission KPIs to shared dashboards. | +| `knowledge_ingest.py` | `instruments/research/` | Normalizes scouting outputs and stores them in `knowledge/custom/main`. | +| `compliance_pack.md` | `docs/policies/` | Canonical policy bundle referenced by Compliance Guardian. | + +### Build Steps +1. **Prompts:** Generate persona files using the template in Section 8 and register them in `agent.system.main.md`. +2. **Extensions:** Implement budget guard + watchdog extensions; ensure alphabetical ordering prefixes reflect execution order. +3. **Instruments:** For each department, add at least one instrument that encapsulates its repetitive workflows (scoring, experimentation, deployment, telemetry). +4. **Scheduling:** Use cron or an orchestration agent to run scouting, budgeting, and telemetry instruments on fixed cadences. +5. **Testing:** Dry-run each instrument via `run_cli.py instruments ` (or equivalent) before enabling full autonomy. +6. **Observability:** Link telemetry outputs to dashboards accessible from the Web UI for optional human monitoring. + +These scaffolds ensure every persona has a dedicated toolkit, observability path, and safety net, reinforcing the low-touch operation goal. + +--- + +## 11. Immersive Collaboration & Visualization UI + +### 11.1 Experience Goals +- **Situational Awareness:** Shared, real-time map of active missions, responsible agents, and workflow state. +- **Low-friction Dialogue:** Zoom-like canvas where agents can “speak,” exchange artifacts, and request clarification without leaving the UI. +- **Replayability:** Session snapshots captured for auditing how decisions were reached. + +### 11.2 Interface Zones +1. **Mission Map (left pane):** Node-link graph (missions → departments → agents) with status colors and tooltips containing KPIs + current LLM. +2. **Collaboration Theater (center):** Spatial meeting room: + - Seats/avatars for participating agents and humans. + - Avatars display role iconography, provider badge, and live transcript bubble. + - Shared whiteboard synced to `logs/board_sessions/.json`. +3. **Command Console (right pane):** Action queue (spawn subordinate, run instrument, adjust behavior) and telemetry gauges (budget, risk, throughput). + +### 11.3 Interaction Mechanics +- **Agent Speech:** Agents stream updates (text + optional TTS) into bubbles; transcripts saved to `logs/ui_sessions/`. +- **Artifact Docking:** Drag artifacts from the `webui` file browser into the whiteboard; objects reference canonical files to avoid duplication. +- **Planning Templates:** Load pre-built canvases (OKR planner, experiment matrix) via instruments for structured workshops. +- **Moderation Controls:** Apex Orchestrator or sponsor can spotlight speakers, freeze the room, or enforce speaking order. + +### 11.4 Implementation Hooks +- Frontend modules (extend `webui/js/`): + - `agentsGraph.js`: d3-force rendering fed by `/api/missions/graph`. + - `collabRoom.js`: WebRTC/WebSocket session manager for avatars, chat, and whiteboard diffing. + - `llmBadges.css`: Visual mapping of model/provider combos. +- Backend additions: + - Streaming endpoint emitting agent lifecycle events (join, speak, artifact shared). + - Session controller persisting meeting metadata + board states into `logs/`. + +--- + +## 12. Multi-LLM Strategy Per Role + +### 12.1 Assignment Matrix +| Persona | Primary Model | Secondary / Fallback | Notes | +| --- | --- | --- | --- | +| Apex Orchestrator | GPT-4.1 / Claude Opus | GPT-4o mini | Needs long context + governance rigor. | +| Portfolio Navigator | Gemini 1.5 Pro | Claude Sonnet | Balanced analysis vs. cost. | +| Research Fellows | Mixtral 8x22B (API) | Local Llama-3.1-70B | High-parallel experimentation. | +| Product Synthesists | GPT-4o mini | Llama-3.1-70B | UX narratives + storytelling. | +| Compliance Guardian | GPT-4o | Claude Opus | Policy/law precision. | +| Telemetry Sentinel | DeepSeek Coder V2 | Local function-calling model | Data summarization + anomaly detection. | + +### 12.2 Routing Logic +- Extension `_15_model_router.py`: + - Reads persona metadata (stored in persona prompt files or `settings.yml`) to pick `preferred_model`. + - Checks provider quotas; if usage >80% or latency spikes, switches to fallback. + - Emits routing decisions to telemetry for monitoring. +- Behavior adjustments can override the router when special handling is needed (e.g., red-team exercises). + +### 12.3 Quality & Cost Monitoring +- Every tool call logs: provider, model, input/output tokens, latency, perceived quality score. +- Telemetry Sentinel aggregates per-persona stats and recommends rebalancing (e.g., shift Research Fellows to local models when load is high). +- Budget Guard extension enforces per-department token ceilings; on breach, router downgrades non-critical personas automatically. + +--- + +## 13. Sandbox Collaboration Environment (MVP) + +### 13.1 Objectives +1. Validate the immersive UI and multi-LLM routing in isolation. +2. Provide a safe arena for agent-agent-human workshops with synthetic missions. +3. Gather UX + performance telemetry before touching production data. + +### 13.2 Sandbox Stack +- **Docker profile `sandbox`:** Launches minimal services + mock integrations. +- **Data:** Synthetic missions, faux knowledge base, isolated memory store at `/sandbox_memory`. +- **Models:** Prefer staged API keys or local open-source models; cap spend via environment variables. +- **Telemetry:** Writes to `logs/sandbox/*` for easy cleanup. + +### 13.3 Core Test Scenarios +| Scenario | Description | Success Criteria | +| --- | --- | --- | +| Planning Summit | 5 personas prioritize synthetic roadmap in collab room. | OKR board saved, transcripts archived, no dropped connections. | +| Research Relay | Research Fellow → Product Synthesist → Engineer handoff using whiteboard artifacts. | Artifacts linked, multi-LLM routing recorded. | +| Customer Preview | Simulated client persona joins, receives demo, leaves feedback captured to memory. | Compliance Guardian verifies messaging vs. policy pack. | + +### 13.4 Exit Criteria +- Stable WebSocket sessions with ≥6 concurrent avatars. +- Cost telemetry within sandbox budget envelope. +- Guardrail extensions successfully flag injected issues. + +--- + +## 14. Roadmap for Productionizing the UI +1. **Design System:** Extend `webui/css` with a “mission control” palette; ensure WCAG AA contrast. +2. **Graph API:** Implement `/api/missions/graph` with caching + permission checks. +3. **Realtime Backbone:** WebSocket gateway + optional WebRTC audio pipeline for live voice “agent briefings.” +4. **Session Recording:** Serialize transcripts, whiteboard diffs, mission decisions into `logs/ui_sessions/.json` and HTML viewer. +5. **Security Model:** JWT-based roles (sponsor, agent, observer) + per-session PIN for external participants. +6. **Rollout:** Sandbox → staging missions → production; enable customer-facing invites only after telemetry + compliance sign-off. + +The UI, multi-LLM routing, and sandbox strategy together enable a testable, graphical collaboration layer where agents and humans coordinate like a virtual R&D control room before expanding to real customer interactions. + +--- + +## 15. Living Documentation & Iterative Improvement Protocol + +### 15.1 Continuous Documentation Streams +- **Mission Diaries:** Every active program (e.g., the Autonomous Listing Service) maintains a rolling log in `docs/programs//journal.md` capturing decisions, blockers, and outcomes per sprint. +- **Agent Telemetry Snapshots:** Telemetry Sentinel exports weekly health summaries (latency, cost, delegation frequency) into `logs/reports/.md`, feeding performance discussions. +- **Prompt Changelog:** Persona prompt updates are versioned under `prompts/super-agency/CHANGELOG.md`, ensuring traceability between behavioral tweaks and observed results. + +### 15.2 Iterative Improvement Loop +1. **Observe:** Collect metrics from mission diaries, telemetry, and customer interactions. +2. **Diagnose:** Portfolio Navigator + relevant Domain Studio review anomalies or opportunities inside the Collaboration Theater. +3. **Design Experiments:** Create improvement hypotheses (e.g., new negotiation playbook for listings) and encode them as: + - Behavior rule adjustments, + - Instrument updates, + - Prompt/Pipeline revisions. +4. **Deploy:** Ship changes through sandbox → staging → production pipeline with automated validation hooks. +5. **Reflect:** Capture outcomes in mission diaries; if successful, promote learnings to `knowledge/custom/main` for reuse. + +### 15.3 Cross-Mission Feedback Sharing +- Quarterly **Agency Retrospectives** aggregate insights from all missions, highlighting reusable playbooks (image enhancement steps, pricing heuristics). +- **Improvement Backlog** stored in `docs/agency_improvements.md` ranks cross-cutting enhancements (e.g., better RAG embeddings, refined UI micro-interactions) with designated owner agents. +- **Auto-Watchdogs** detect regressions (e.g., rising unanswered inquiries) and open improvement tasks automatically via behavior adjustment instructions, ensuring the agency self-tunes without waiting for human prompts. + +### 15.4 Co-Development with New Services +When launching new services (like the Autonomous Listing Service): +- Embed the mission diary + improvement backlog from day one. +- Reuse the iterative loop above so the service and the core agency evolve together. +- Require each release to document: decision rationale, metrics impacted, and next hypothesis, guaranteeing a living blueprint as the system scales. + +This protocol keeps the agency’s documentation synchronized with real operations while institutionalizing an experiment-driven mindset for every new service it incubates. \ No newline at end of file diff --git a/docs/token_compression_protocol.md b/docs/token_compression_protocol.md new file mode 100644 index 0000000000..646efcf823 --- /dev/null +++ b/docs/token_compression_protocol.md @@ -0,0 +1,303 @@ +# Token Compression Protocol (TCP) + +This document defines an easy-to-implement protocol for compressing LLM prompts +and responses while preserving the original encoding and a persistent context. +It is designed to run as a local TCP service and integrate cleanly with browser +extensions via a lightweight native-host bridge. + +## Goals + +- Encode user prompts in base64. +- Accept model responses in base54. +- Decode responses back into the original encoding (utf-8, ascii, etc). +- Maintain a persistent, base64-rendered context across conversations. +- Provide token savings diagnostics via `**show savings**` and `**show total**`. +- Keep the wire format simple: newline-delimited JSON over TCP. + +## Server Overview + +The TCP server lives at: + +- Module: `python/helpers/token_compression_protocol.py` +- Default host: `127.0.0.1` +- Default port: `7543` +- Context dataset: `memory/token_compression_context.json` + +Run it locally: + +```bash +python3 /workspace/python/helpers/token_compression_protocol.py +``` + +The server accepts one JSON object per line and returns one JSON object per line. + +## Base54 Alphabet + +The response payload uses base54 with this alphabet: + +``` +123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstyz +``` + +This avoids ambiguous characters (0, O, I, l) and a few lower-case letters to +hit an even base of 54. + +## Transport Protocol (TCP) + +Each request is a single JSON line terminated by `\n` (LF). Each response is +also a single JSON line terminated by `\n`. + +### Common Envelope + +All responses include: + +```json +{ + "ok": true, + "result": { ... } +} +``` + +Errors are returned as: + +```json +{ + "ok": false, + "error": "error_code", + "detail": "optional detail" +} +``` + +### Actions + +#### `prompt` + +Encode a user prompt to base64, update context, and return the new context. + +Request: + +```json +{ + "action": "prompt", + "conversation_id": "optional", + "text": "user prompt text", + "encoding": "utf-8", + "language": "en" +} +``` + +Notes: +- If `conversation_id` is omitted, the server generates one. +- `encoding` and `language` are stored and reused for the conversation. +- If `**show savings**` or `**show total**` is present in `text`, it is stripped + before encoding and applied to the next `response`. + +Response: + +```json +{ + "ok": true, + "result": { + "conversation_id": "uuid", + "encoding": "utf-8", + "language": "en", + "encoded_prompt_b64": "SGVsbG8=", + "context_b64": "dXNlcjogSGVsbG8=", + "context_tokens": { "raw": 2, "encoded": 2, "saved": 0 }, + "prompt_tokens": { "raw": 2, "encoded": 2, "saved": 0 }, + "savings_request": { "show_savings": true, "show_total": false } + } +} +``` + +#### `response` + +Submit a base54 response payload from the model, decode it, and update context. + +Request: + +```json +{ + "action": "response", + "conversation_id": "uuid", + "payload_b54": "base54response" +} +``` + +Response (base form): + +```json +{ + "ok": true, + "result": { + "conversation_id": "uuid", + "encoding": "utf-8", + "language": "en", + "response_b54": "base54response", + "decoded_text": "model response", + "response_tokens": { "raw": 4, "encoded": 3, "saved": 1 }, + "context_b64": "dXNlcjogSGVsbG8K...==", + "context_tokens": { "raw": 6, "encoded": 5, "saved": 1 } + } +} +``` + +If `**show savings**` or `**show total**` was set in the last prompt, the +response includes a `savings` object, `tagline`, and `decoded_text_with_tagline`: + +```json +{ + "ok": true, + "result": { + "...": "...", + "tagline": "Token savings (prompt/response/context/combined): 0/1/1/2.", + "decoded_text_with_tagline": "model response\nToken savings ...", + "savings": { + "prompt": { "raw": 2, "encoded": 2, "saved": 0 }, + "response": { "raw": 4, "encoded": 3, "saved": 1 }, + "context": { "raw": 6, "encoded": 5, "saved": 1 }, + "combined_saved": 2, + "totals": { + "prompt": { "raw": 10, "encoded": 10, "saved": 0 }, + "response": { "raw": 20, "encoded": 18, "saved": 2 }, + "context": { "raw": 30, "encoded": 25, "saved": 5 }, + "combined_saved": 7 + } + } + } +} +``` + +#### `context_get` + +Fetch the current base64 context for a conversation (or all conversations). + +Request: + +```json +{ + "action": "context_get", + "conversation_id": "uuid" +} +``` + +Response: + +```json +{ + "ok": true, + "result": { + "conversation_id": "uuid", + "context_b64": "dXNlcjogSGVsbG8=", + "encoding": "utf-8", + "language": "en", + "context_tokens": { "raw": 2, "encoded": 2, "saved": 0 } + } +} +``` + +#### `context_reset` + +Delete a conversation from the dataset. + +Request: + +```json +{ + "action": "context_reset", + "conversation_id": "uuid" +} +``` + +#### `ping` + +Request: + +```json +{ "action": "ping" } +``` + +Response: + +```json +{ "ok": true, "result": { "message": "pong" } } +``` + +## Browser Extension Integration + +Browsers cannot open raw TCP sockets directly. The easiest integration pattern +is a lightweight local bridge that the extension can message. + +### Option A: Native Messaging Host (Recommended) + +Use the browser's native messaging API to launch a small helper process that +connects to the TCP server and forwards JSON lines. + +Flow: + +1. Extension sends a JSON message to the native host. +2. Native host writes the JSON line to `127.0.0.1:7543`. +3. Native host reads the JSON response line and returns it to the extension. + +Advantages: +- Works in Chrome and Firefox. +- No CORS or HTTP server needed. +- Minimal bridging logic (just pass-through JSON lines). + +Native host pseudo-code: + +```python +import json, socket, sys + +def tcp_exchange(payload): + data = json.dumps(payload).encode("utf-8") + b"\n" + with socket.create_connection(("127.0.0.1", 7543)) as sock: + sock.sendall(data) + response = sock.recv(1024 * 1024).split(b"\n", 1)[0] + return json.loads(response.decode("utf-8")) +``` + +### Option B: Local HTTP/WS Bridge + +If you prefer `fetch` or WebSocket from the extension, run a local bridge that +translates HTTP/WS into the TCP line protocol: + +- `POST /tcp` -> send JSON line over TCP, return JSON response +- `GET /context/:conversation_id` -> map to `context_get` + +This is a thin shim and keeps the TCP protocol unchanged. + +## Example End-to-End Session + +1) Encode prompt: + +```json +{"action":"prompt","text":"Summarize this. **show savings**","encoding":"utf-8","language":"en"} +``` + +2) Send `encoded_prompt_b64` to the model (outside TCP server). + +3) Encode model output to base54 (client-side), then send: + +```json +{"action":"response","conversation_id":"...","payload_b54":"..."} +``` + +4) Receive decoded text plus savings tagline. + +## Data Persistence + +Context is stored in `memory/token_compression_context.json`. The server keeps a +background thread that refreshes and persists the context every few seconds. + +## Security Notes + +- Run the TCP server on `127.0.0.1` only. +- Treat `context_b64` as sensitive; it contains full conversation history. +- Use the native messaging approach if you need strict extension isolation. + +## Troubleshooting + +- `missing_payload_b54`: Ensure you send base54 for responses, not base64. +- `invalid_base54`: Check the alphabet and strip any non-base54 characters. +- `unknown_conversation_id`: Use the `conversation_id` returned by `prompt`. diff --git a/python/helpers/token_compression_protocol.py b/python/helpers/token_compression_protocol.py new file mode 100644 index 0000000000..13cc2ad6d8 --- /dev/null +++ b/python/helpers/token_compression_protocol.py @@ -0,0 +1,554 @@ +import base64 +import json +import os +import re +import socketserver +import threading +import time +import uuid +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Tuple + +from python.helpers import files, tokens + + +BASE54_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstyz" +BASE54_INDEX = {char: idx for idx, char in enumerate(BASE54_ALPHABET)} +BASE54_BASE = len(BASE54_ALPHABET) + +CONTROL_TAG_SHOW_SAVINGS = "**show savings**" +CONTROL_TAG_SHOW_TOTAL = "**show total**" + + +def _safe_count_tokens(text: str) -> int: + if not text: + return 0 + try: + return tokens.count_tokens(text) + except Exception: + return max(1, len(text.split())) + + +def _token_stats(raw_text: str, encoded_text: str) -> Dict[str, int]: + raw_tokens = _safe_count_tokens(raw_text) + encoded_tokens = _safe_count_tokens(encoded_text) + saved = raw_tokens - encoded_tokens + if saved < 0: + saved = 0 + return { + "raw": raw_tokens, + "encoded": encoded_tokens, + "saved": saved, + } + + +def _strip_control_tags(text: str) -> Tuple[str, bool, bool]: + show_savings = False + show_total = False + if not text: + return text, show_savings, show_total + + if re.search(re.escape(CONTROL_TAG_SHOW_SAVINGS), text, flags=re.IGNORECASE): + show_savings = True + text = re.sub( + re.escape(CONTROL_TAG_SHOW_SAVINGS), "", text, flags=re.IGNORECASE + ) + if re.search(re.escape(CONTROL_TAG_SHOW_TOTAL), text, flags=re.IGNORECASE): + show_total = True + show_savings = True + text = re.sub( + re.escape(CONTROL_TAG_SHOW_TOTAL), "", text, flags=re.IGNORECASE + ) + + return text.strip(), show_savings, show_total + + +def b54encode(payload: bytes) -> str: + if not payload: + return "" + num = int.from_bytes(payload, "big") + encoded: List[str] = [] + while num > 0: + num, rem = divmod(num, BASE54_BASE) + encoded.append(BASE54_ALPHABET[rem]) + pad = 0 + for byte in payload: + if byte == 0: + pad += 1 + else: + break + encoded_str = "".join(reversed(encoded)) if encoded else "" + return (BASE54_ALPHABET[0] * pad) + encoded_str + + +def b54decode(payload: str) -> bytes: + if payload == "": + return b"" + num = 0 + for char in payload: + if char not in BASE54_INDEX: + raise ValueError(f"Invalid base54 character: {char!r}") + num = num * BASE54_BASE + BASE54_INDEX[char] + pad = 0 + for char in payload: + if char == BASE54_ALPHABET[0]: + pad += 1 + else: + break + decoded = b"" + if num > 0: + byte_len = (num.bit_length() + 7) // 8 + decoded = num.to_bytes(byte_len, "big") + return (b"\x00" * pad) + decoded + + +def _b64encode_text(text: str, encoding: str) -> str: + return base64.b64encode(text.encode(encoding, errors="replace")).decode("ascii") + + +def _context_text(messages: List[Dict[str, str]]) -> str: + return "\n".join(f"{entry['role']}: {entry['text']}" for entry in messages).strip() + + +@dataclass +class ConversationState: + conversation_id: str + encoding: str = "utf-8" + language: str = "unknown" + messages: List[Dict[str, str]] = field(default_factory=list) + context_b64: str = "" + context_tokens: Dict[str, int] = field(default_factory=dict) + prompt_tokens_raw: int = 0 + prompt_tokens_encoded: int = 0 + response_tokens_raw: int = 0 + response_tokens_encoded: int = 0 + last_prompt_stats: Dict[str, int] = field(default_factory=dict) + last_response_stats: Dict[str, int] = field(default_factory=dict) + pending_show_savings: bool = False + pending_show_total: bool = False + + def to_dict(self) -> Dict[str, Any]: + return { + "conversation_id": self.conversation_id, + "encoding": self.encoding, + "language": self.language, + "messages": self.messages, + "context_b64": self.context_b64, + "context_tokens": self.context_tokens, + "prompt_tokens_raw": self.prompt_tokens_raw, + "prompt_tokens_encoded": self.prompt_tokens_encoded, + "response_tokens_raw": self.response_tokens_raw, + "response_tokens_encoded": self.response_tokens_encoded, + } + + @classmethod + def from_dict(cls, payload: Dict[str, Any]) -> "ConversationState": + return cls( + conversation_id=payload.get("conversation_id", ""), + encoding=payload.get("encoding", "utf-8"), + language=payload.get("language", "unknown"), + messages=payload.get("messages", []), + context_b64=payload.get("context_b64", ""), + context_tokens=payload.get("context_tokens", {}), + prompt_tokens_raw=payload.get("prompt_tokens_raw", 0), + prompt_tokens_encoded=payload.get("prompt_tokens_encoded", 0), + response_tokens_raw=payload.get("response_tokens_raw", 0), + response_tokens_encoded=payload.get("response_tokens_encoded", 0), + ) + + +class ContextStore: + def __init__(self, dataset_path: str, refresh_interval: float = 5.0): + self.dataset_path = dataset_path + self.refresh_interval = refresh_interval + self._lock = threading.Lock() + self._dirty = False + self._conversations: Dict[str, ConversationState] = {} + self._stop_event = threading.Event() + self._load() + self._thread = threading.Thread( + target=self._maintenance_loop, + name="tcp-context-maintainer", + daemon=True, + ) + self._thread.start() + + def _load(self) -> None: + if not os.path.exists(self.dataset_path): + return + try: + with open(self.dataset_path, "r", encoding="utf-8") as handle: + data = json.load(handle) + except (OSError, json.JSONDecodeError): + return + conversations = data.get("conversations", {}) + for conv_id, payload in conversations.items(): + state = ConversationState.from_dict(payload) + if not state.conversation_id: + state.conversation_id = conv_id + self._conversations[conv_id] = state + + def _maintenance_loop(self) -> None: + while not self._stop_event.wait(self.refresh_interval): + self._flush_if_dirty() + + def _flush_if_dirty(self) -> None: + with self._lock: + if not self._dirty: + return + snapshot = self._snapshot_locked() + self._dirty = False + self._persist_snapshot(snapshot) + + def _snapshot_locked(self) -> Dict[str, Any]: + for state in self._conversations.values(): + self._refresh_context_locked(state) + return { + "updated_at": time.strftime("%Y-%m-%d %H:%M:%S"), + "conversations": { + conv_id: state.to_dict() + for conv_id, state in self._conversations.items() + }, + } + + def _persist_snapshot(self, snapshot: Dict[str, Any]) -> None: + os.makedirs(os.path.dirname(self.dataset_path), exist_ok=True) + tmp_path = f"{self.dataset_path}.tmp" + with open(tmp_path, "w", encoding="utf-8") as handle: + json.dump(snapshot, handle, ensure_ascii=True, indent=2) + os.replace(tmp_path, self.dataset_path) + + def stop(self) -> None: + self._stop_event.set() + self._thread.join(timeout=self.refresh_interval) + self._flush_if_dirty() + + def get_or_create( + self, + conversation_id: Optional[str], + encoding: Optional[str], + language: Optional[str], + ) -> ConversationState: + with self._lock: + if not conversation_id: + conversation_id = str(uuid.uuid4()) + state = self._conversations.get(conversation_id) + if state is None: + state = ConversationState(conversation_id=conversation_id) + self._conversations[conversation_id] = state + if encoding: + state.encoding = encoding + if language: + state.language = language + return state + + def list_contexts(self) -> Dict[str, Dict[str, Any]]: + with self._lock: + contexts = {} + for conv_id, state in self._conversations.items(): + self._refresh_context_locked(state) + contexts[conv_id] = { + "context_b64": state.context_b64, + "encoding": state.encoding, + "language": state.language, + "context_tokens": state.context_tokens, + } + return contexts + + def get_context(self, conversation_id: str) -> Optional[Dict[str, Any]]: + with self._lock: + state = self._conversations.get(conversation_id) + if not state: + return None + self._refresh_context_locked(state) + return { + "conversation_id": state.conversation_id, + "context_b64": state.context_b64, + "encoding": state.encoding, + "language": state.language, + "context_tokens": state.context_tokens, + } + + def record_prompt( + self, + conversation_id: Optional[str], + text: str, + encoding: Optional[str], + language: Optional[str], + ) -> Dict[str, Any]: + state = self.get_or_create(conversation_id, encoding, language) + clean_text, show_savings, show_total = _strip_control_tags(text) + encoded_prompt = _b64encode_text(clean_text, state.encoding) + prompt_stats = _token_stats(clean_text, encoded_prompt) + with self._lock: + state.messages.append({"role": "user", "text": clean_text}) + state.prompt_tokens_raw += prompt_stats["raw"] + state.prompt_tokens_encoded += prompt_stats["encoded"] + state.last_prompt_stats = prompt_stats + state.pending_show_savings = show_savings or show_total + state.pending_show_total = show_total + self._refresh_context_locked(state) + self._dirty = True + response = { + "conversation_id": state.conversation_id, + "encoding": state.encoding, + "language": state.language, + "encoded_prompt_b64": encoded_prompt, + "context_b64": state.context_b64, + "context_tokens": state.context_tokens, + "prompt_tokens": prompt_stats, + "savings_request": { + "show_savings": state.pending_show_savings, + "show_total": state.pending_show_total, + }, + } + return response + + def record_response( + self, + conversation_id: str, + payload_b54: str, + ) -> Dict[str, Any]: + with self._lock: + state = self._conversations.get(conversation_id) + if not state: + raise KeyError("Unknown conversation_id") + encoding = state.encoding + language = state.language + + decoded_bytes = b54decode(payload_b54) + decoded_text = decoded_bytes.decode(encoding, errors="replace") + response_stats = _token_stats(decoded_text, payload_b54) + + with self._lock: + state.messages.append({"role": "assistant", "text": decoded_text}) + state.response_tokens_raw += response_stats["raw"] + state.response_tokens_encoded += response_stats["encoded"] + state.last_response_stats = response_stats + self._refresh_context_locked(state) + savings_payload = None + tagline = None + decoded_text_with_tagline = None + if state.pending_show_savings: + savings_payload = self._build_savings_payload(state) + tagline = self._format_tagline( + savings_payload, + include_total=state.pending_show_total, + ) + decoded_text_with_tagline = ( + decoded_text + "\n" + tagline if decoded_text else tagline + ) + state.pending_show_savings = False + state.pending_show_total = False + self._dirty = True + response = { + "conversation_id": state.conversation_id, + "encoding": encoding, + "language": language, + "response_b54": payload_b54, + "decoded_text": decoded_text, + "response_tokens": response_stats, + "context_b64": state.context_b64, + "context_tokens": state.context_tokens, + } + if savings_payload: + response["savings"] = savings_payload + if tagline: + response["tagline"] = tagline + response["decoded_text_with_tagline"] = decoded_text_with_tagline + return response + + def _refresh_context_locked(self, state: ConversationState) -> None: + context_text = _context_text(state.messages) + state.context_b64 = _b64encode_text(context_text, state.encoding) + state.context_tokens = _token_stats(context_text, state.context_b64) + + def _build_savings_payload(self, state: ConversationState) -> Dict[str, Any]: + prompt_stats = state.last_prompt_stats or {"raw": 0, "encoded": 0, "saved": 0} + response_stats = state.last_response_stats or { + "raw": 0, + "encoded": 0, + "saved": 0, + } + context_stats = state.context_tokens or {"raw": 0, "encoded": 0, "saved": 0} + combined_saved = ( + prompt_stats.get("saved", 0) + + response_stats.get("saved", 0) + + context_stats.get("saved", 0) + ) + totals = { + "prompt": { + "raw": state.prompt_tokens_raw, + "encoded": state.prompt_tokens_encoded, + "saved": max( + 0, state.prompt_tokens_raw - state.prompt_tokens_encoded + ), + }, + "response": { + "raw": state.response_tokens_raw, + "encoded": state.response_tokens_encoded, + "saved": max( + 0, state.response_tokens_raw - state.response_tokens_encoded + ), + }, + "context": context_stats, + } + totals["combined_saved"] = ( + totals["prompt"]["saved"] + + totals["response"]["saved"] + + totals["context"]["saved"] + ) + return { + "prompt": prompt_stats, + "response": response_stats, + "context": context_stats, + "combined_saved": combined_saved, + "totals": totals, + } + + def _format_tagline(self, savings: Dict[str, Any], include_total: bool) -> str: + prompt_saved = savings["prompt"]["saved"] + response_saved = savings["response"]["saved"] + context_saved = savings["context"]["saved"] + combined_saved = savings["combined_saved"] + tagline = ( + "Token savings (prompt/response/context/combined): " + f"{prompt_saved}/{response_saved}/{context_saved}/{combined_saved}." + ) + if include_total: + totals = savings.get("totals", {}) + totals_prompt = totals.get("prompt", {}).get("saved", 0) + totals_response = totals.get("response", {}).get("saved", 0) + totals_context = totals.get("context", {}).get("saved", 0) + totals_combined = totals.get("combined_saved", 0) + tagline += ( + " Total savings (prompt/response/context/combined): " + f"{totals_prompt}/{totals_response}/{totals_context}/{totals_combined}." + ) + return tagline + + +class TokenCompressionProtocolProcessor: + def __init__(self, store: ContextStore): + self.store = store + + def handle(self, payload: Dict[str, Any]) -> Dict[str, Any]: + action = payload.get("action") + if not action: + return {"ok": False, "error": "missing_action"} + + if action == "prompt": + text = payload.get("text", "") + if not isinstance(text, str) or text == "": + return {"ok": False, "error": "missing_text"} + response = self.store.record_prompt( + conversation_id=payload.get("conversation_id"), + text=text, + encoding=payload.get("encoding"), + language=payload.get("language"), + ) + return {"ok": True, "result": response} + + if action == "response": + conversation_id = payload.get("conversation_id") + if not conversation_id: + return {"ok": False, "error": "missing_conversation_id"} + payload_b54 = payload.get("payload_b54", "") + if not isinstance(payload_b54, str) or payload_b54 == "": + return {"ok": False, "error": "missing_payload_b54"} + try: + response = self.store.record_response( + conversation_id=conversation_id, + payload_b54=payload_b54, + ) + except KeyError: + return {"ok": False, "error": "unknown_conversation_id"} + except ValueError as exc: + return {"ok": False, "error": "invalid_base54", "detail": str(exc)} + return {"ok": True, "result": response} + + if action == "context_get": + conversation_id = payload.get("conversation_id") + if conversation_id: + context = self.store.get_context(conversation_id) + if not context: + return {"ok": False, "error": "unknown_conversation_id"} + return {"ok": True, "result": context} + return {"ok": True, "result": {"contexts": self.store.list_contexts()}} + + if action == "context_reset": + conversation_id = payload.get("conversation_id") + if not conversation_id: + return {"ok": False, "error": "missing_conversation_id"} + with self.store._lock: + if conversation_id in self.store._conversations: + del self.store._conversations[conversation_id] + self.store._dirty = True + return {"ok": True, "result": {"conversation_id": conversation_id}} + return {"ok": False, "error": "unknown_conversation_id"} + + if action == "ping": + return {"ok": True, "result": {"message": "pong"}} + + return {"ok": False, "error": "unknown_action"} + + +class TokenCompressionTCPServer(socketserver.ThreadingTCPServer): + allow_reuse_address = True + daemon_threads = True + + def __init__(self, server_address, RequestHandlerClass, processor): + super().__init__(server_address, RequestHandlerClass) + self.processor = processor + + +class TokenCompressionRequestHandler(socketserver.StreamRequestHandler): + def handle(self) -> None: + while True: + raw_line = self.rfile.readline() + if not raw_line: + break + raw_line = raw_line.strip() + if not raw_line: + continue + try: + request = json.loads(raw_line.decode("utf-8")) + except json.JSONDecodeError as exc: + self._send({"ok": False, "error": "invalid_json", "detail": str(exc)}) + continue + if not isinstance(request, dict): + self._send({"ok": False, "error": "invalid_payload"}) + continue + response = self.server.processor.handle(request) + self._send(response) + + def _send(self, payload: Dict[str, Any]) -> None: + encoded = json.dumps(payload, ensure_ascii=True).encode("utf-8") + b"\n" + self.wfile.write(encoded) + + +def run_tcp_server( + host: str = "127.0.0.1", + port: int = 7543, + dataset_path: Optional[str] = None, + refresh_interval: float = 5.0, +) -> None: + dataset_path = dataset_path or files.get_abs_path( + "memory", "token_compression_context.json" + ) + store = ContextStore(dataset_path=dataset_path, refresh_interval=refresh_interval) + processor = TokenCompressionProtocolProcessor(store) + server = TokenCompressionTCPServer( + (host, port), TokenCompressionRequestHandler, processor + ) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + store.stop() + server.server_close() + + +if __name__ == "__main__": + run_tcp_server() diff --git a/run_simple_gui.py b/run_simple_gui.py new file mode 100644 index 0000000000..557c1f0580 --- /dev/null +++ b/run_simple_gui.py @@ -0,0 +1,376 @@ +import json +import os +import threading +import tkinter as tk +from tkinter import ttk +from tkinter.scrolledtext import ScrolledText +from urllib import error as url_error +from urllib import request as url_request + +from python.helpers import tokens + + +class SimpleChatGUI: + def __init__(self, root: tk.Tk) -> None: + self.root = root + self.root.title("Simple LLM Chat") + self.root.minsize(980, 640) + + self.messages: list[dict[str, str]] = [] + self.context_text = "" + + self.provider_var = tk.StringVar(value="Ollama") + self.model_var = tk.StringVar( + value=os.environ.get("OLLAMA_MODEL", "llama3") + ) + self.api_key_var = tk.StringVar(value=os.environ.get("OPENROUTER_API_KEY", "")) + self.status_var = tk.StringVar(value="Ready") + self.prompt_tokens_var = tk.StringVar(value="Prompt tokens: 0") + self.response_tokens_var = tk.StringVar(value="Response tokens: 0") + self.context_tokens_var = tk.StringVar(value="Context tokens: 0") + self.total_tokens_var = tk.StringVar(value="Total tokens: 0") + + self._build_layout() + self._bind_events() + + def _build_layout(self) -> None: + main_frame = ttk.Frame(self.root, padding=10) + main_frame.pack(fill="both", expand=True) + + main_frame.columnconfigure(0, weight=3) + main_frame.columnconfigure(1, weight=1) + main_frame.rowconfigure(0, weight=1) + + chat_frame = ttk.Frame(main_frame) + chat_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10)) + chat_frame.rowconfigure(0, weight=1) + chat_frame.columnconfigure(0, weight=1) + + self.chat_display = ScrolledText(chat_frame, wrap="word", state="disabled") + self.chat_display.grid(row=0, column=0, columnspan=2, sticky="nsew") + + self.input_text = ScrolledText(chat_frame, height=6, wrap="word") + self.input_text.grid(row=1, column=0, sticky="nsew", pady=(10, 0)) + + button_frame = ttk.Frame(chat_frame) + button_frame.grid(row=1, column=1, sticky="se", padx=(10, 0), pady=(10, 0)) + + self.send_button = ttk.Button(button_frame, text="Send", command=self.on_send) + self.send_button.pack(fill="x", pady=(0, 6)) + + self.clear_button = ttk.Button( + button_frame, text="New Chat", command=self.on_clear + ) + self.clear_button.pack(fill="x") + + sidebar = ttk.Frame(main_frame) + sidebar.grid(row=0, column=1, sticky="nsew") + sidebar.rowconfigure(2, weight=1) + sidebar.columnconfigure(0, weight=1) + + settings_frame = ttk.LabelFrame(sidebar, text="Settings") + settings_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) + settings_frame.columnconfigure(1, weight=1) + + ttk.Label(settings_frame, text="Provider").grid( + row=0, column=0, sticky="w", padx=8, pady=4 + ) + provider_combo = ttk.Combobox( + settings_frame, + textvariable=self.provider_var, + values=["Ollama", "OpenRouter"], + state="readonly", + ) + provider_combo.grid(row=0, column=1, sticky="ew", padx=8, pady=4) + + ttk.Label(settings_frame, text="Model").grid( + row=1, column=0, sticky="w", padx=8, pady=4 + ) + self.model_combo = ttk.Combobox( + settings_frame, textvariable=self.model_var, values=[] + ) + self.model_combo.grid(row=1, column=1, sticky="ew", padx=8, pady=4) + + self.refresh_button = ttk.Button( + settings_frame, text="Load Ollama Models", command=self.on_refresh_models + ) + self.refresh_button.grid(row=2, column=0, columnspan=2, sticky="ew", padx=8, pady=4) + + ttk.Label(settings_frame, text="OpenRouter API Key").grid( + row=3, column=0, sticky="w", padx=8, pady=4 + ) + self.api_key_entry = ttk.Entry( + settings_frame, textvariable=self.api_key_var, show="*" + ) + self.api_key_entry.grid(row=3, column=1, sticky="ew", padx=8, pady=4) + + context_frame = ttk.LabelFrame(sidebar, text="Conversation Context") + context_frame.grid(row=1, column=0, sticky="nsew", pady=(0, 10)) + context_frame.columnconfigure(0, weight=1) + context_frame.rowconfigure(0, weight=1) + + self.context_display = ScrolledText( + context_frame, wrap="word", height=12, state="disabled" + ) + self.context_display.grid(row=0, column=0, sticky="nsew", padx=6, pady=6) + + tokens_frame = ttk.LabelFrame(sidebar, text="Token Counts") + tokens_frame.grid(row=2, column=0, sticky="ew") + + ttk.Label(tokens_frame, textvariable=self.prompt_tokens_var).pack( + anchor="w", padx=8, pady=2 + ) + ttk.Label(tokens_frame, textvariable=self.response_tokens_var).pack( + anchor="w", padx=8, pady=2 + ) + ttk.Label(tokens_frame, textvariable=self.context_tokens_var).pack( + anchor="w", padx=8, pady=2 + ) + ttk.Label(tokens_frame, textvariable=self.total_tokens_var).pack( + anchor="w", padx=8, pady=2 + ) + + status_bar = ttk.Label( + self.root, textvariable=self.status_var, anchor="w" + ) + status_bar.pack(fill="x") + + self._refresh_provider_ui() + + def _bind_events(self) -> None: + self.input_text.bind("", lambda _evt: self.on_send()) + self.provider_var.trace_add("write", lambda *_: self._refresh_provider_ui()) + + def _refresh_provider_ui(self) -> None: + provider = self.provider_var.get().strip() + if provider == "Ollama": + self.refresh_button.state(["!disabled"]) + self.api_key_entry.state(["disabled"]) + else: + self.api_key_entry.state(["!disabled"]) + if not self.model_var.get().strip(): + self.model_var.set( + os.environ.get("OPENROUTER_MODEL", "openai/gpt-4o-mini") + ) + self.refresh_button.state(["disabled"]) + + def on_refresh_models(self) -> None: + if self.provider_var.get() != "Ollama": + return + self._set_status("Loading Ollama models...") + threading.Thread(target=self._load_ollama_models, daemon=True).start() + + def _load_ollama_models(self) -> None: + try: + response = self._post_json( + "http://localhost:11434/api/tags", payload={}, method="GET" + ) + models = sorted([entry["name"] for entry in response.get("models", [])]) + except Exception as exc: + self.root.after( + 0, lambda: self._set_status(f"Failed to load Ollama models: {exc}") + ) + return + + def update_models() -> None: + self.model_combo["values"] = models + if models and self.model_var.get() not in models: + self.model_var.set(models[0]) + self._set_status("Ollama models loaded.") + + self.root.after(0, update_models) + + def on_send(self) -> None: + user_text = self.input_text.get("1.0", "end").strip() + if not user_text: + return + if not self.model_var.get().strip(): + self._set_status("Please enter a model name.") + return + + if self.provider_var.get() == "OpenRouter" and not self.api_key_var.get().strip(): + self._set_status("OpenRouter API key is required.") + return + + self._append_chat("user", user_text) + self.messages.append({"role": "user", "content": user_text}) + self._update_context_display() + self._update_prompt_tokens(user_text) + self.response_tokens_var.set("Response tokens: 0") + self._set_status("Sending...") + self._set_busy(True) + + self.input_text.delete("1.0", "end") + + messages_snapshot = list(self.messages) + provider = self.provider_var.get() + model = self.model_var.get().strip() + api_key = self.api_key_var.get().strip() + + threading.Thread( + target=self._call_model, + args=(provider, model, api_key, messages_snapshot), + daemon=True, + ).start() + + def _call_model( + self, + provider: str, + model: str, + api_key: str, + messages: list[dict[str, str]], + ) -> None: + try: + if provider == "Ollama": + response_text = self._call_ollama(model, messages) + else: + response_text = self._call_openrouter(model, api_key, messages) + except Exception as exc: + self.root.after(0, lambda: self._handle_error(exc)) + return + self.root.after(0, lambda: self._handle_response(response_text)) + + def _handle_response(self, response_text: str) -> None: + self._append_chat("assistant", response_text) + self.messages.append({"role": "assistant", "content": response_text}) + self._update_context_display() + self._update_token_counts(response_text) + self._set_busy(False) + self._set_status("Ready") + + def _handle_error(self, exc: Exception) -> None: + self._append_chat("assistant", f"[Error] {exc}") + self._set_busy(False) + self._set_status("Error while calling model.") + + def on_clear(self) -> None: + self.messages.clear() + self.context_text = "" + self.chat_display.configure(state="normal") + self.chat_display.delete("1.0", "end") + self.chat_display.configure(state="disabled") + self._append_chat("assistant", "[New conversation started]") + self._update_context_display() + self.prompt_tokens_var.set("Prompt tokens: 0") + self.response_tokens_var.set("Response tokens: 0") + self.context_tokens_var.set("Context tokens: 0") + self.total_tokens_var.set("Total tokens: 0") + + def _append_chat(self, role: str, content: str) -> None: + self.chat_display.configure(state="normal") + label = "You" if role == "user" else "Assistant" + self.chat_display.insert("end", f"{label}: {content}\n\n") + self.chat_display.configure(state="disabled") + self.chat_display.see("end") + + def _update_context_display(self) -> None: + self.context_text = "\n".join( + f"{message['role']}: {message['content']}" for message in self.messages + ) + self.context_display.configure(state="normal") + self.context_display.delete("1.0", "end") + self.context_display.insert("end", self.context_text) + self.context_display.configure(state="disabled") + self._update_context_tokens() + + def _update_token_counts(self, response_text: str) -> None: + prompt_text = self.messages[-2]["content"] if len(self.messages) >= 2 else "" + prompt_tokens = self._safe_count_tokens(prompt_text) + response_tokens = self._safe_count_tokens(response_text) + context_tokens = self._safe_count_tokens(self.context_text) + total_tokens = context_tokens + + self.prompt_tokens_var.set(f"Prompt tokens: {prompt_tokens}") + self.response_tokens_var.set(f"Response tokens: {response_tokens}") + self.context_tokens_var.set(f"Context tokens: {context_tokens}") + self.total_tokens_var.set(f"Total tokens: {total_tokens}") + + def _update_prompt_tokens(self, prompt_text: str) -> None: + prompt_tokens = self._safe_count_tokens(prompt_text) + self.prompt_tokens_var.set(f"Prompt tokens: {prompt_tokens}") + + def _update_context_tokens(self) -> None: + context_tokens = self._safe_count_tokens(self.context_text) + self.context_tokens_var.set(f"Context tokens: {context_tokens}") + self.total_tokens_var.set(f"Total tokens: {context_tokens}") + + def _set_status(self, message: str) -> None: + self.status_var.set(message) + + def _set_busy(self, busy: bool) -> None: + if busy: + self.send_button.state(["disabled"]) + else: + self.send_button.state(["!disabled"]) + + def _safe_count_tokens(self, text: str) -> int: + if not text: + return 0 + try: + return tokens.count_tokens(text) + except Exception: + return len(text.split()) + + def _call_ollama(self, model: str, messages: list[dict[str, str]]) -> str: + payload = {"model": model, "messages": messages, "stream": False} + response = self._post_json("http://localhost:11434/api/chat", payload=payload) + message = response.get("message", {}) + return str(message.get("content", "")).strip() + + def _call_openrouter( + self, model: str, api_key: str, messages: list[dict[str, str]] + ) -> str: + payload = { + "model": model, + "messages": messages, + "temperature": 0.7, + } + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "HTTP-Referer": "http://localhost", + "X-Title": "Agent Zero Simple GUI", + } + response = self._post_json( + "https://openrouter.ai/api/v1/chat/completions", + payload=payload, + headers=headers, + ) + choices = response.get("choices", []) + if not choices: + return "" + message = choices[0].get("message", {}) + return str(message.get("content", "")).strip() + + def _post_json( + self, + url: str, + payload: dict[str, object], + headers: dict[str, str] | None = None, + method: str = "POST", + ) -> dict[str, object]: + data = None if method == "GET" else json.dumps(payload).encode("utf-8") + req_headers = {"Content-Type": "application/json"} + if headers: + req_headers.update(headers) + req = url_request.Request(url, data=data, headers=req_headers, method=method) + try: + with url_request.urlopen(req, timeout=120) as resp: + body = resp.read().decode("utf-8") + except url_error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc + except url_error.URLError as exc: + raise RuntimeError(f"Connection error: {exc}") from exc + return json.loads(body) if body else {} + + +def main() -> None: + root = tk.Tk() + app = SimpleChatGUI(root) + app._append_chat("assistant", "Welcome! Enter a prompt to begin.") + root.mainloop() + + +if __name__ == "__main__": + main() diff --git a/services/autonomous_listing/Dockerfile b/services/autonomous_listing/Dockerfile new file mode 100644 index 0000000000..b9fdbfb210 --- /dev/null +++ b/services/autonomous_listing/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/services/autonomous_listing/README.md b/services/autonomous_listing/README.md new file mode 100644 index 0000000000..0b0a40f963 --- /dev/null +++ b/services/autonomous_listing/README.md @@ -0,0 +1,36 @@ +# Autonomous Listing Service (MVP Scaffold) + +This folder contains a FastAPI-based microservice that simulates the AI pipeline defined in `docs/autonomous_listing_service.md`. It is not production-ready yet, but it provides a runnable skeleton that: + +1. Accepts listing requests with seller notes, assets, preferences, and target platforms. +2. Runs through placeholder pipelines for image enhancement, marketing copy, and multi-platform publishing. +3. Returns a structured preview containing the listing status, suggested price, generated description, and enhanced asset URIs. + +## Quick Start + +```bash +cd services/autonomous_listing +python -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +uvicorn app.main:app --reload +``` + +POST a sample request: + +```bash +curl -X POST http://localhost:8000/listings \ + -H "Content-Type: application/json" \ + -d '{ + "raw_description": "Mid-century walnut coffee table, gentle wear, includes glass top.", + "category": "furniture", + "location": "Austin, TX", + "assets": [{"source_uri": "https://example.com/photo1.jpg"}], + "target_platforms": ["craigslist", "mercari"], + "preferences": {"tone": "premium", "target_price": 350} + }' +``` + +## Next Steps +- Replace the stubbed pipelines (`image_enhancer`, `description_generator`, `publisher`) with real AI agents and marketplace adapters. +- Connect to object storage for asset handling and to the knowledge/memory layers described in the main blueprint. +- Embed telemetry + mission diary hooks so the service feeds the agency’s iterative improvement loop. diff --git a/services/autonomous_listing/app/__init__.py b/services/autonomous_listing/app/__init__.py new file mode 100644 index 0000000000..c9f1f8219d --- /dev/null +++ b/services/autonomous_listing/app/__init__.py @@ -0,0 +1,3 @@ +from . import schemas + +__all__ = ["schemas"] diff --git a/services/autonomous_listing/app/main.py b/services/autonomous_listing/app/main.py new file mode 100644 index 0000000000..477bd5c14a --- /dev/null +++ b/services/autonomous_listing/app/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI + +from . import schemas +from .services.orchestrator import ListingOrchestrator +from .services.pipelines.description_generator import DescriptionGenerator +from .services.pipelines.image_enhancer import ImageEnhancer +from .services.pipelines.publisher import ChannelPublisher + +app = FastAPI( + title="Autonomous Listing Service", + description="Transforms seller inputs into multi-channel listings via AI pipelines.", + version="0.1.0", +) + +orchestrator = ListingOrchestrator( + enhancer=ImageEnhancer(), + copywriter=DescriptionGenerator(), + publisher=ChannelPublisher(), +) + + +@app.get("/health") +async def health_check() -> dict: + return {"status": "ok"} + + +@app.post("/listings", response_model=schemas.ListingResponse) +async def create_listing(payload: schemas.ListingRequest) -> schemas.ListingResponse: + """ + Entry point for creating a new autonomous listing. + Downstream pipelines are mocked for now and should be replaced with actual AI services. + """ + + return await orchestrator.create_listing(payload) diff --git a/services/autonomous_listing/app/schemas.py b/services/autonomous_listing/app/schemas.py new file mode 100644 index 0000000000..f40d2fba2e --- /dev/null +++ b/services/autonomous_listing/app/schemas.py @@ -0,0 +1,93 @@ +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, Field, HttpUrl + + +class PlatformEnum(str, Enum): + craigslist = "craigslist" + mercari = "mercari" + nextdoor = "nextdoor" + offerup = "offerup" + custom = "custom" + + +class SellerPreference(BaseModel): + tone: Optional[str] = Field( + None, + description="Preferred copy tone (e.g., premium, playful, concise)", + ) + min_price: Optional[float] = Field( + None, + description="Lowest acceptable price for auto-negotiation guardrails", + ) + target_price: Optional[float] = Field( + None, + description="Ideal listing price suggested to pricing agent", + ) + pickup_only: Optional[bool] = Field( + False, description="If true, restrict listings to local pickup" + ) + + +class ListingAsset(BaseModel): + source_uri: Optional[HttpUrl] = Field( + None, description="Publicly accessible URL for the uploaded asset." + ) + base64_payload: Optional[str] = Field( + None, + description="Optional base64 encoded asset body when direct upload is used.", + ) + caption: Optional[str] = Field(None, description="User-provided caption or note.") + + +class ListingRequest(BaseModel): + title_hint: Optional[str] = Field( + None, description="Optional working title supplied by the seller." + ) + raw_description: str = Field( + ..., description="Free-form notes describing the item, condition, and story." + ) + category: Optional[str] = Field( + None, description="High-level category to help routing (e.g., furniture)." + ) + location: Optional[str] = Field( + None, description="City/region for localized marketplaces." + ) + assets: List[ListingAsset] = Field( + default_factory=list, description="Collection of reference photos/videos." + ) + target_platforms: List[PlatformEnum] = Field( + default_factory=lambda: [PlatformEnum.craigslist], + description="Marketplaces that should receive this listing.", + ) + preferences: SellerPreference = Field( + default_factory=SellerPreference, + description="Preferences controlling tone, pricing, negotiations.", + ) + + +class ListingStatus(BaseModel): + listing_id: str = Field(..., description="Internal tracking identifier.") + state: str = Field( + ..., + description="State machine stage (ingesting, enhancing, drafting, publishing, live, closed).", + ) + platforms_live: List[PlatformEnum] = Field( + default_factory=list, description="Platforms with confirmed publication." + ) + notes: Optional[str] = Field(None, description="Additional context for the seller.") + + +class ListingResponse(BaseModel): + status: ListingStatus + recommended_price: Optional[float] = Field( + None, description="Initial suggestion from valuation pipeline." + ) + preview_description: Optional[str] = Field( + None, description="First-pass marketing copy preview." + ) + enhanced_assets: List[str] = Field( + default_factory=list, + description="URIs for enhanced images stored in object storage.", + ) diff --git a/services/autonomous_listing/app/services/orchestrator.py b/services/autonomous_listing/app/services/orchestrator.py new file mode 100644 index 0000000000..c256cd3f8f --- /dev/null +++ b/services/autonomous_listing/app/services/orchestrator.py @@ -0,0 +1,67 @@ +import uuid +from typing import List, Tuple + +from .pipelines.description_generator import DescriptionGenerator +from .pipelines.image_enhancer import ImageEnhancer +from .pipelines.publisher import ChannelPublisher +from .. import schemas + + +class ListingOrchestrator: + """Thin coordination layer that chains the enhancement, copywriting, pricing and publishing steps.""" + + def __init__( + self, + enhancer: ImageEnhancer, + copywriter: DescriptionGenerator, + publisher: ChannelPublisher, + ) -> None: + self._enhancer = enhancer + self._copywriter = copywriter + self._publisher = publisher + + async def create_listing( + self, payload: schemas.ListingRequest + ) -> schemas.ListingResponse: + listing_id = str(uuid.uuid4()) + enhanced_assets = await self._enhancer.process(listing_id, payload.assets) + + preview_description, suggested_price = await self._copywriter.generate( + listing_id=listing_id, + request=payload, + enhanced_assets=enhanced_assets, + ) + + publish_results = await self._publisher.schedule_publication( + listing_id=listing_id, + request=payload, + enhanced_assets=enhanced_assets, + description=preview_description, + recommended_price=suggested_price, + ) + + status = schemas.ListingStatus( + listing_id=listing_id, + state="publishing" if publish_results.pending else "live", + platforms_live=publish_results.confirmed_platforms, + notes=publish_results.notes, + ) + + return schemas.ListingResponse( + status=status, + recommended_price=suggested_price, + preview_description=preview_description, + enhanced_assets=enhanced_assets, + ) + + +class PublishResult: + def __init__( + self, + pending: bool, + confirmed_platforms: List[schemas.PlatformEnum], + notes: str = "", + ) -> None: + self.pending = pending + self.confirmed_platforms = confirmed_platforms + self.notes = notes diff --git a/services/autonomous_listing/app/services/pipelines/description_generator.py b/services/autonomous_listing/app/services/pipelines/description_generator.py new file mode 100644 index 0000000000..530bb27be9 --- /dev/null +++ b/services/autonomous_listing/app/services/pipelines/description_generator.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import asyncio +import random +from typing import List, Tuple + +from ... import schemas + + +class DescriptionGenerator: + """ + Simplified marketing copy generator. + Replace with actual LLM/RAG pipeline wired to provider SDKs. + """ + + async def generate( + self, + listing_id: str, + request: schemas.ListingRequest, + enhanced_assets: List[str], + ) -> Tuple[str, float]: + await asyncio.sleep(0.1) + hero_line = request.title_hint or "Stunning find ready for a new home" + detail = request.raw_description.strip() + asset_note = ( + f"Includes {len(enhanced_assets)} professionally enhanced photos." + if enhanced_assets + else "Image enhancement pending." + ) + + narrative = ( + f"{hero_line}\n\n" + f"{detail}\n\n" + f"{asset_note} Curated for {', '.join([p.value for p in request.target_platforms])}." + ) + + suggested_price = request.preferences.target_price or round( + random.uniform(20, 200), 2 + ) + + return narrative, suggested_price diff --git a/services/autonomous_listing/app/services/pipelines/image_enhancer.py b/services/autonomous_listing/app/services/pipelines/image_enhancer.py new file mode 100644 index 0000000000..b5b9499a73 --- /dev/null +++ b/services/autonomous_listing/app/services/pipelines/image_enhancer.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import asyncio +from typing import List + +from ... import schemas + + +class ImageEnhancer: + """ + Placeholder enhancer that emulates an asynchronous vision pipeline. + In production, this would call GPU-backed services (Real-ESRGAN, ControlNet, etc.). + """ + + async def process( + self, listing_id: str, assets: List[schemas.ListingAsset] + ) -> List[str]: + if not assets: + return [] + + await asyncio.sleep(0.1) # simulate async workload + enhanced_uris = [ + asset.source_uri or f"s3://placeholder/{listing_id}/{idx}.jpg" + for idx, asset in enumerate(assets) + ] + return enhanced_uris diff --git a/services/autonomous_listing/app/services/pipelines/publisher.py b/services/autonomous_listing/app/services/pipelines/publisher.py new file mode 100644 index 0000000000..fa10082b30 --- /dev/null +++ b/services/autonomous_listing/app/services/pipelines/publisher.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from typing import List + +from ... import schemas +from ..orchestrator import PublishResult + + +@dataclass +class PublicationTask: + platform: schemas.PlatformEnum + status: str + reference_id: str + + +class ChannelPublisher: + """ + Stubbed marketplace publisher. + Replace with real adapters (Craigslist headless automation, Mercari API, etc.). + """ + + async def schedule_publication( + self, + listing_id: str, + request: schemas.ListingRequest, + enhanced_assets: List[str], + description: str, + recommended_price: float, + ) -> PublishResult: + await asyncio.sleep(0.1) + confirmed: List[schemas.PlatformEnum] = [] + pending = False + + for platform in request.target_platforms: + if platform in (schemas.PlatformEnum.craigslist, schemas.PlatformEnum.nextdoor): + # emulate asynchronous approval queues for certain platforms + pending = True + else: + confirmed.append(platform) + + notes = ( + "Some platforms require manual review before going live." + if pending + else "All requested platforms confirmed." + ) + + return PublishResult( + pending=pending, + confirmed_platforms=confirmed, + notes=notes, + ) diff --git a/services/autonomous_listing/requirements.txt b/services/autonomous_listing/requirements.txt new file mode 100644 index 0000000000..1556727492 --- /dev/null +++ b/services/autonomous_listing/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +pydantic==1.10.18