Skip to content

Commit 18897e8

Browse files
committed
feat(ad-diagnosis): add storage layer, export tools, credibility CIs, and diagnostic charts
Storage abstraction (src/core/storage/): - Strategy Pattern: S3CompatibleBackend (R2/S3/MinIO) and LocalHTTPBackend - CLOUDFLARE_R2_ACCOUNT_ID auto-builds R2 endpoint URL - Switchable via STORAGE_BACKEND env var, no caller changes needed Output tools (src/mcp/servers/output/tools/): - export_html: markdown to styled HTML with image upload via storage backend - export_csv: real DictWriter implementation with storage upload fallback Ad diagnosis credibility dimensions: - Statistical sufficiency flag: orders_reliability (low/medium/high) - ACOS 95% CI via Wilson score interval on CVR - ITS/DML/CausalImpact effect confidence intervals - Within-sample directional backtest (hit rate vs delta_orders) - LP keyword|MATCH_TYPE output bug fixed Diagnostic charts (6 per ASIN, uploaded to R2, embedded in report): - Daily performance trend: spend bar + orders line + ACOS with CI band - ITS causal fit: pre-trend, counterfactual, shaded effect area, CI title - Keyword ACOS x Orders quadrant scatter (bubble = spend) - Placement ACOS vs configured bid adjustment grouped bar - Inventory burn-down curve with stockout date and risk zone - Competitor price distribution box-plot with own price line Docs + deploy: README .env table updated, ARCHITECTURE/DEV_GUIDE storage section added, deploy_ubuntu.sh .env template extended with full storage block. Ignore src/core/telemetry/step_history.json (runtime-generated).
1 parent cb90a29 commit 18897e8

15 files changed

Lines changed: 1219 additions & 81 deletions

File tree

AWS/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,4 @@ output/*.csv
7878
output/*.json
7979
tests/*.csv
8080
tests/test_input.csv
81+
src/core/telemetry/step_history.json

AWS/README.md

Lines changed: 72 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,63 +56,111 @@ It employs a **Hybrid Intelligence** model:
5656

5757
```dotenv
5858
# ── LLM Providers ──────────────────────────────────────────
59-
GEMINI_API_KEY=your_gemini_api_key_here
60-
ANTHROPIC_API_KEY=your_anthropic_api_key_here # Required only when using Claude
61-
62-
# Default provider: "gemini" | "claude"
6359
DEFAULT_LLM_PROVIDER=gemini
60+
GEMINI_API_KEY=
61+
ANTHROPIC_API_KEY=
62+
DEEPSEEK_API_KEY=
63+
LOCAL_MODEL_PATH=models/llm/qwen2.5-3b-instruct-q4_k_m.gguf
64+
MAX_LLM_OUTPUT_TOKENS=
6465
65-
# Path to local GGUF model file (optional, for offline mode)
66-
LOCAL_MODEL_PATH=models/llm/your_model.gguf
66+
# ── Amazon Ads API ─────────────────────────────────────────
67+
AMAZON_ADS_DEFAULT_STORE=US
68+
AMAZON_ADS_CLIENT_ID=
69+
AMAZON_ADS_CLIENT_SECRET=
70+
AMAZON_ADS_REFRESH_TOKEN_US=
71+
AMAZON_ADS_PROFILE_ID_US=
72+
AMAZON_ADS_FALLBACK_ASIN_US=
73+
74+
# ── Amazon SP-API / LWA ────────────────────────────────────
75+
AMAZON_LWA_CLIENT_ID=
76+
AMAZON_LWA_CLIENT_SECRET=
77+
AMAZON_SP_API_REFRESH_TOKEN_US=
6778
6879
# ── Feishu / Lark Bots ─────────────────────────────────────
6980
# Naming rule: FEISHU_{BOT_NAME_UPPER}_{FIELD}
70-
# Add as many bots as needed by following the same pattern.
7181
7282
# amazon_bot (primary bot)
7383
FEISHU_AMAZON_BOT_APP_ID=
7484
FEISHU_AMAZON_BOT_APP_SECRET=
75-
FEISHU_AMAZON_BOT_USER_ACCESS_TOKEN= # Optional: for user-level Bitable access
76-
FEISHU_AMAZON_BOT_WEBHOOK_URL= # Optional: for incoming webhook messages
85+
FEISHU_AMAZON_BOT_USER_ACCESS_TOKEN=
86+
FEISHU_AMAZON_BOT_WEBHOOK_URL=
7787
7888
# test_bot (secondary / staging bot)
7989
FEISHU_TEST_BOT_APP_ID=
8090
FEISHU_TEST_BOT_APP_SECRET=
8191
FEISHU_TEST_BOT_USER_ACCESS_TOKEN=
8292
FEISHU_TEST_BOT_WEBHOOK_URL=
8393
84-
# ── Third-party Services ───────────────────────────────────
94+
# ── Third-party Market Data ────────────────────────────────
8595
SELLERSPRITE_EMAIL=
8696
SELLERSPRITE_PASSWORD=
87-
XIYOU_PHONE=
88-
89-
# ── Amazon Advertising API (LWA) ───────────────────────────
90-
AMAZON_ADS_CLIENT_ID=
91-
AMAZON_ADS_CLIENT_SECRET=
92-
AMAZON_ADS_DEFAULT_STORE=US
93-
# Multiple stores supported: AMAZON_ADS_REFRESH_TOKEN_{STORE}, AMAZON_ADS_PROFILE_ID_{STORE}
94-
AMAZON_ADS_REFRESH_TOKEN_US=
95-
AMAZON_ADS_PROFILE_ID_US=
97+
XIYOUZHAOCI_PHONE=
98+
LINGXING_ACCOUNT=
99+
LINGXING_PASSWORD=
96100
97101
# ── Infrastructure ─────────────────────────────────────────
98-
# Omit to use local JSON-file cache; set to enable Redis backend
99102
REDIS_URL=redis://localhost:6379
103+
SERVER_IP=
104+
SERVER_USER=
105+
106+
# ── Object Storage ─────────────────────────────────────────
107+
# Backend: s3_compatible (R2 / S3 / MinIO) | local_http (VPS nginx)
108+
STORAGE_BACKEND=s3_compatible
109+
110+
# Cloudflare R2 (recommended)
111+
CLOUDFLARE_R2_ACCOUNT_ID= # endpoint auto-built from this
112+
STORAGE_ACCESS_KEY_ID=
113+
STORAGE_SECRET_ACCESS_KEY=
114+
STORAGE_BUCKET_NAME=
115+
STORAGE_PUBLIC_URL= # e.g. https://your-domain.com or https://pub-<hash>.r2.dev
116+
STORAGE_REGION=auto
117+
118+
# AWS S3 (omit CLOUDFLARE_R2_ACCOUNT_ID, set real region)
119+
# STORAGE_REGION=us-east-1
120+
121+
# MinIO / self-hosted (set explicit endpoint)
122+
# STORAGE_ENDPOINT_URL=https://minio.yourdomain.com
123+
124+
# VPS local directory + nginx
125+
# STORAGE_BACKEND=local_http
126+
# STORAGE_LOCAL_DIR=/var/www/files
100127
```
101128

102129
| Variable | Required | Description |
103130
|---|---|---|
104131
| `GEMINI_API_KEY` | When using Gemini | Google AI Studio API key |
105132
| `ANTHROPIC_API_KEY` | When using Claude | Anthropic Console API key |
133+
| `DEEPSEEK_API_KEY` | When using DeepSeek | DeepSeek API key |
106134
| `DEFAULT_LLM_PROVIDER` | No (default: `gemini`) | Active LLM backend |
107135
| `LOCAL_MODEL_PATH` | No | Path to GGUF model for offline inference |
108-
| `FEISHU_*_APP_ID` | For Feishu bot | Lark Open Platform App ID |
109-
| `FEISHU_*_APP_SECRET` | For Feishu bot | Lark Open Platform App Secret |
110-
| `FEISHU_*_USER_ACCESS_TOKEN` | No | User-level token for Bitable write access |
111-
| `FEISHU_*_WEBHOOK_URL` | No | Incoming webhook URL for the bot |
136+
| `MAX_LLM_OUTPUT_TOKENS` | No | Cap on LLM response length |
112137
| `AMAZON_ADS_CLIENT_ID` | For Ads API | Login with Amazon Client ID |
113138
| `AMAZON_ADS_CLIENT_SECRET` | For Ads API | Login with Amazon Client Secret |
114139
| `AMAZON_ADS_REFRESH_TOKEN_*` | For Ads API | OAuth2 Refresh Token per store |
115140
| `AMAZON_ADS_PROFILE_ID_*` | For Ads API | Advertising Profile ID per store |
141+
| `AMAZON_ADS_FALLBACK_ASIN_US` | No | ASIN used for test/fallback metric calls |
142+
| `AMAZON_LWA_CLIENT_ID` | For SP-API | LWA Client ID |
143+
| `AMAZON_LWA_CLIENT_SECRET` | For SP-API | LWA Client Secret |
144+
| `AMAZON_SP_API_REFRESH_TOKEN_US` | For SP-API | SP-API OAuth2 Refresh Token |
145+
| `FEISHU_*_APP_ID` | For Feishu bot | Lark Open Platform App ID |
146+
| `FEISHU_*_APP_SECRET` | For Feishu bot | Lark Open Platform App Secret |
147+
| `FEISHU_*_USER_ACCESS_TOKEN` | No | User-level token for Bitable write access |
148+
| `FEISHU_*_WEBHOOK_URL` | No | Incoming webhook URL for the bot |
149+
| `SELLERSPRITE_EMAIL` | For SellerSprite | Account email |
150+
| `XIYOUZHAOCI_PHONE` | For Xiyouzhaoci | Account phone number |
151+
| `LINGXING_ACCOUNT` | For Lingxing | Account username |
152+
| `REDIS_URL` | No | Enables Redis backend in `DataCache` |
153+
| `SERVER_IP` | No | VPS IP shown in SSH tunnel hints |
154+
| `SERVER_USER` | No | VPS username shown in SSH tunnel hints |
155+
| `STORAGE_BACKEND` | No (default: `s3_compatible`) | Storage driver: `s3_compatible` or `local_http` |
156+
| `CLOUDFLARE_R2_ACCOUNT_ID` | For R2 | R2 account ID; auto-builds endpoint URL |
157+
| `STORAGE_ACCESS_KEY_ID` | For S3/R2/MinIO | S3-compatible access key |
158+
| `STORAGE_SECRET_ACCESS_KEY` | For S3/R2/MinIO | S3-compatible secret key |
159+
| `STORAGE_BUCKET_NAME` | For S3/R2/MinIO | Target bucket name |
160+
| `STORAGE_PUBLIC_URL` | For S3/R2/MinIO | Base URL for public file access |
161+
| `STORAGE_REGION` | No (default: `auto`) | Region (`auto` for R2/MinIO, real region for S3) |
162+
| `STORAGE_ENDPOINT_URL` | For MinIO/Backblaze | Explicit S3-compatible endpoint override |
163+
| `STORAGE_LOCAL_DIR` | For `local_http` | Local directory to write files |
116164

117165
---
118166

AWS/docs/ARCHITECTURE.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,26 @@ The AWS (Amazon Web Scraper) V2 project is a **Hybrid Intelligence Agentic Platf
216216
| | | | (Local JSON lookup)| | (Direct IM attach) | |
217217
| | calc_profit | | restriction_check | | populate_bitable | |
218218
| | calc_fba_fee | | epa_check | | send_local_file | |
219-
| | (+ category bench) | | patent_risk_calc | | send_url_file | |
220-
| | | | | | send_data_file | |
219+
| | (+ category bench) | | patent_risk_calc | | export_md | |
220+
| | | | | | export_html (→ R2) | |
221+
| | | | | | export_csv (→ R2) | |
221222
| +--------------------+ +--------------------+ +--------------------+ |
223+
| |
224+
| ── src/core/storage/ (Strategy Pattern — swappable per .env) ────────── |
225+
| |
226+
| StorageBackend (ABC) |
227+
| upload(key, bytes, content_type) → public_https_url |
228+
| upload_file(key, path) → public_https_url |
229+
| delete(key) |
230+
| |
231+
| +──────────────────────+──────────────────────────────────────────────+ |
232+
| | S3CompatibleBackend | R2 / AWS S3 / MinIO — same boto3 client, | |
233+
| | | only endpoint_url changes | |
234+
| +──────────────────────+──────────────────────────────────────────────+ |
235+
| | LocalHTTPBackend | VPS dir + nginx/caddy; no cloud dependency | |
236+
| +──────────────────────+──────────────────────────────────────────────+ |
237+
| |
238+
| Switching backend: set STORAGE_BACKEND env var; zero code changes. |
222239
|
223240
v
224241
+==============================================================================+
@@ -543,16 +560,19 @@ The AWS (Amazon Web Scraper) V2 project is a **Hybrid Intelligence Agentic Platf
543560
544561
545562
================================================================================
546-
EXTENSION RULES (5-Dimensional Orthogonal)
563+
EXTENSION RULES (6-Dimensional Orthogonal)
547564
================================================================================
548565
549566
New Entry Point EntryPoint adapter + Gateway register Zero change elsewhere
550567
New Workflow WorkflowRegistry.register(name, build_fn) Zero change elsewhere
551568
New Data Source New MCP Server + Tool Registry register Zero business change
552569
New Output Format Callback subclass + CallbackFactory reg Zero change elsewhere
553570
Switch Model Intelligence Router providers register Zero business change
571+
Switch Storage Set STORAGE_BACKEND env var Zero code change
572+
(s3_compatible → R2/S3/MinIO; local_http → VPS nginx)
573+
Add backend: subclass StorageBackend, 1-line factory entry
554574
555-
Result: Five extension dimensions are orthogonal; changing one leaves others intact
575+
Result: Six extension dimensions are orthogonal; changing one leaves others intact
556576
557577
558578
================================================================================

AWS/docs/DEV_GUIDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,24 @@ This guide reflects the **Domain-Driven Design (DDD)** and **Dual Orchestration*
152152
## 5. Directory Mapping (Summary)
153153
154154
* `src/core/`: Kernel, Models, Telemetry, and shared Utils (Proxy, Cookies, Context).
155+
* `src/core/storage/`: **Storage abstraction layer** (Strategy Pattern). Swap backends via `STORAGE_BACKEND` env var — no code changes.
156+
* `S3CompatibleBackend` — Cloudflare R2 / AWS S3 / MinIO (same boto3 client, different endpoint)
157+
* `LocalHTTPBackend` — VPS local directory served by nginx/caddy
158+
* Usage: `from src.core.storage import get_storage_backend; url = storage.upload(key, bytes, mime)`
155159
* `src/entry/`: Entry adapters (CLI, Feishu, etc.).
156160
* `src/gateway/`: Auth, Rate Limiting, and Unified Dispatching.
157161
* `src/jobs/`: Job management, Checkpoints, and Callbacks.
158162
* `src/mcp/servers/`: Microservices providing specific tools.
163+
* `src/mcp/servers/output/tools/export_html.py` — Converts markdown/HTML to styled HTML file; optionally uploads images via storage backend.
164+
* `src/mcp/servers/output/tools/export_csv.py` — Exports records to CSV; uploads via storage backend (falls back to local file if unconfigured).
159165
* `src/registry/`: The central hub for Tool, Resource, and Prompt discovery.
160166
* `src/intelligence/`: LLM Providers, Routing, and AI Processors.
161167
* `src/workflows/`: Sequential, deterministic engine.
162168
* `src/agents/`: Autonomous, LLM-driven reasoning.
169+
170+
## 6. Adding a New Storage Backend
171+
172+
1. Subclass `StorageBackend` in `src/core/storage/your_backend.py` — implement `upload`, `upload_file`, `delete`.
173+
2. Add a branch in `src/core/storage/__init__.py` `get_storage_backend()`.
174+
3. Set `STORAGE_BACKEND=your_backend` in `.env`.
175+
4. No changes to `export_html`, `export_csv`, or any caller.

AWS/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ scikit-learn
2424
numpy
2525
pandas
2626
pycausalimpact
27+
boto3
28+
markdown
2729

2830
# Encryption
2931
cryptography

AWS/scripts/deploy_ubuntu.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,38 @@ LINGXING_PASSWORD=
141141
REDIS_URL=redis://localhost:6379
142142
SERVER_IP=
143143
SERVER_USER=
144+
145+
# ── Object Storage (export_html / export_csv image/file upload) ─────────
146+
# Backend choices: s3_compatible (R2 / S3 / MinIO) | local_http (VPS nginx)
147+
STORAGE_BACKEND=s3_compatible
148+
149+
# --- Cloudflare R2 (recommended) ---
150+
CLOUDFLARE_R2_ACCOUNT_ID= # from R2 dashboard; endpoint auto-built from this
151+
STORAGE_ACCESS_KEY_ID= # R2 API token → Access Key ID
152+
STORAGE_SECRET_ACCESS_KEY= # R2 API token → Secret Access Key
153+
STORAGE_BUCKET_NAME= # your R2 bucket name
154+
STORAGE_PUBLIC_URL= # e.g. https://pub-<hash>.r2.dev or custom domain
155+
STORAGE_REGION=auto
156+
157+
# --- AWS S3 (omit CLOUDFLARE_R2_ACCOUNT_ID, set region) ---
158+
# STORAGE_ACCESS_KEY_ID=
159+
# STORAGE_SECRET_ACCESS_KEY=
160+
# STORAGE_BUCKET_NAME=
161+
# STORAGE_PUBLIC_URL=https://<bucket>.s3.<region>.amazonaws.com
162+
# STORAGE_REGION=us-east-1
163+
164+
# --- MinIO / self-hosted S3 (set explicit endpoint) ---
165+
# STORAGE_ENDPOINT_URL=https://minio.yourdomain.com
166+
# STORAGE_ACCESS_KEY_ID=
167+
# STORAGE_SECRET_ACCESS_KEY=
168+
# STORAGE_BUCKET_NAME=
169+
# STORAGE_PUBLIC_URL=https://files.yourdomain.com
170+
# STORAGE_REGION=auto
171+
172+
# --- VPS local directory + nginx (no cloud dependency) ---
173+
# STORAGE_BACKEND=local_http
174+
# STORAGE_LOCAL_DIR=/var/www/files
175+
# STORAGE_PUBLIC_URL=https://files.yourdomain.com
144176
EOF
145177
echo "⚠️ .env created — edit it and fill in all required values before running the app."
146178
else

AWS/src/core/storage/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
"""
3+
Storage abstraction layer.
4+
5+
Usage:
6+
from src.core.storage import get_storage_backend
7+
storage = get_storage_backend()
8+
url = storage.upload("images/chart.png", png_bytes, "image/png")
9+
10+
Backend is selected by STORAGE_BACKEND env var:
11+
s3_compatible (default) — R2 / S3 / MinIO
12+
local_http — VPS directory + nginx/caddy
13+
"""
14+
import os
15+
from .base import StorageBackend
16+
17+
18+
def get_storage_backend() -> StorageBackend:
19+
backend = os.getenv("STORAGE_BACKEND", "s3_compatible").lower()
20+
if backend == "s3_compatible":
21+
from .s3_compatible import S3CompatibleBackend
22+
return S3CompatibleBackend()
23+
if backend == "local_http":
24+
from .local_http import LocalHTTPBackend
25+
return LocalHTTPBackend()
26+
raise ValueError(
27+
f"Unknown STORAGE_BACKEND={backend!r}. "
28+
"Supported: 's3_compatible', 'local_http'."
29+
)
30+
31+
32+
__all__ = ["StorageBackend", "get_storage_backend"]

AWS/src/core/storage/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import annotations
2+
from abc import ABC, abstractmethod
3+
4+
5+
class StorageBackend(ABC):
6+
"""
7+
Minimal interface for public-URL file storage.
8+
9+
Implementations: S3CompatibleBackend (R2 / S3 / MinIO)
10+
LocalHTTPBackend (VPS dir + nginx/caddy)
11+
"""
12+
13+
@abstractmethod
14+
def upload(self, key: str, data: bytes, content_type: str = "application/octet-stream") -> str:
15+
"""Upload bytes and return the public HTTPS URL."""
16+
17+
@abstractmethod
18+
def upload_file(self, key: str, file_path: str, content_type: str = "application/octet-stream") -> str:
19+
"""Upload a local file and return the public HTTPS URL."""
20+
21+
@abstractmethod
22+
def delete(self, key: str) -> None:
23+
"""Delete an object (best-effort; implementations may no-op)."""

AWS/src/core/storage/local_http.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from __future__ import annotations
2+
"""
3+
Local-HTTP storage backend.
4+
5+
Files are written to a directory on the VPS that is served by nginx/caddy
6+
over HTTPS. No S3 dependency needed.
7+
8+
Required env vars:
9+
STORAGE_LOCAL_DIR absolute path served by nginx, e.g. /var/www/files
10+
STORAGE_PUBLIC_URL matching public base URL, e.g. https://files.yourdomain.com
11+
12+
nginx config example:
13+
server {
14+
listen 443 ssl;
15+
server_name files.yourdomain.com;
16+
root /var/www/files;
17+
location / { autoindex off; }
18+
}
19+
"""
20+
import logging
21+
import os
22+
from .base import StorageBackend
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
class LocalHTTPBackend(StorageBackend):
28+
def __init__(
29+
self,
30+
local_dir: str | None = None,
31+
public_url: str | None = None,
32+
):
33+
self._dir = local_dir or os.environ["STORAGE_LOCAL_DIR"]
34+
self._public_url = (public_url or os.environ["STORAGE_PUBLIC_URL"]).rstrip("/")
35+
os.makedirs(self._dir, exist_ok=True)
36+
37+
def upload(self, key: str, data: bytes, content_type: str = "application/octet-stream") -> str:
38+
dest = os.path.join(self._dir, key)
39+
os.makedirs(os.path.dirname(dest), exist_ok=True)
40+
with open(dest, "wb") as f:
41+
f.write(data)
42+
url = f"{self._public_url}/{key}"
43+
logger.info(f"[storage] written {key} ({len(data)} bytes) → {url}")
44+
return url
45+
46+
def upload_file(self, key: str, file_path: str, content_type: str = "application/octet-stream") -> str:
47+
with open(file_path, "rb") as f:
48+
return self.upload(key, f.read(), content_type)
49+
50+
def delete(self, key: str) -> None:
51+
path = os.path.join(self._dir, key)
52+
try:
53+
os.remove(path)
54+
logger.info(f"[storage] deleted {path}")
55+
except FileNotFoundError:
56+
pass
57+
except Exception as e:
58+
logger.warning(f"[storage] delete {path} failed (ignored): {e}")

0 commit comments

Comments
 (0)