cp500 commited on
Commit
7a2bd01
·
verified ·
1 Parent(s): 9cfd958

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +23 -12
  2. app.py +273 -0
  3. pipeline.html +1072 -0
  4. requirements.txt +7 -0
README.md CHANGED
@@ -1,12 +1,23 @@
1
- ---
2
- title: Auto Ontology
3
- emoji: 🐢
4
- colorFrom: yellow
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.8.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Auto-Ontology — Automotive Market Intelligence
3
+ emoji: 🏎️
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: "5.31.0"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: cc-by-4.0
11
+ datasets:
12
+ - cp500/auto-ontology
13
+ ---
14
+
15
+ # Auto-Ontology — Automotive Market Intelligence
16
+
17
+ Explore a hypergraph of **94,671 market signals** connected to **1,261 vehicles** from the NHTSA vPIC registry.
18
+
19
+ **Pipeline tab** — Scrollytelling walkthrough from Common Crawl to structural analysis, with D3.js visualizations.
20
+
21
+ **Chatbot tab** — Ask questions about the automotive market using a Strands Agent backed by the dataset.
22
+
23
+ Built as an AWS Automotive workshop. Dataset: [cp500/auto-ontology](https://huggingface.co/datasets/cp500/auto-ontology)
app.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Auto-Ontology — Automotive Market Intelligence
3
+ HuggingFace Space: Gradio Blocks with pipeline narrative + Strands Agent chatbot.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+
9
+ import gradio as gr
10
+ import pandas as pd
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Load dataset (parquet files from the HF dataset repo)
14
+ # ---------------------------------------------------------------------------
15
+
16
+ DATA_DIR = Path(__file__).parent / "data"
17
+ HF_DATASET = "cp500/auto-ontology"
18
+
19
+
20
+ def _load_parquet(subdir: str, table: str) -> pd.DataFrame | None:
21
+ """Load a parquet table — try local first, then HF hub."""
22
+ local = DATA_DIR / subdir / f"{table}.parquet"
23
+ if local.exists():
24
+ return pd.read_parquet(local)
25
+ try:
26
+ return pd.read_parquet(f"hf://datasets/{HF_DATASET}/data/{subdir}/{table}.parquet")
27
+ except Exception:
28
+ return None
29
+
30
+
31
+ # Lazy-load dataframes
32
+ _cache: dict[str, pd.DataFrame | None] = {}
33
+
34
+
35
+ def get_df(subdir: str, table: str) -> pd.DataFrame:
36
+ key = f"{subdir}/{table}"
37
+ if key not in _cache:
38
+ _cache[key] = _load_parquet(subdir, table)
39
+ df = _cache[key]
40
+ if df is None:
41
+ raise ValueError(f"Table {key} not available")
42
+ return df
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Strands Agent tools — query the parquet dataset
47
+ # ---------------------------------------------------------------------------
48
+
49
+ from strands import Agent, tool
50
+ from strands.models.openai import OpenAIModel
51
+
52
+
53
+ @tool
54
+ def search_products(make: str = "", model: str = "", year: int = 0) -> str:
55
+ """Search the product index for vehicles by make, model, and/or year.
56
+ Returns matching vehicles with their IDs, make, model, year, and body class."""
57
+ df = get_df("hypergraph", "product_index")
58
+ mask = pd.Series(True, index=df.index)
59
+ if make:
60
+ mask &= df["make"].str.contains(make, case=False, na=False)
61
+ if model:
62
+ mask &= df["model"].str.contains(model, case=False, na=False)
63
+ if year:
64
+ mask &= df["model_year"] == year
65
+ results = df[mask].head(20)
66
+ if results.empty:
67
+ return "No products found matching the criteria."
68
+ return results.to_markdown(index=False)
69
+
70
+
71
+ @tool
72
+ def browse_signals(domain: str = "", sentiment: str = "", keyword: str = "") -> str:
73
+ """Browse market signals. Filter by L1 domain code (P/T/C/F/S/R/M/ST),
74
+ sentiment (bullish/bearish/neutral/mixed), or keyword in signal name.
75
+ Returns up to 15 matching signals."""
76
+ si = get_df("hypergraph", "signal_index")
77
+ nodes = get_df("hypergraph", "nodes")
78
+ # Merge to get signal names
79
+ signals = si.merge(nodes[nodes["node_type"] == "Signal"][["id", "name"]],
80
+ left_on="signal_id", right_on="id", how="left")
81
+ mask = pd.Series(True, index=signals.index)
82
+ if domain:
83
+ mask &= signals["domain"].str.upper() == domain.upper()
84
+ if sentiment:
85
+ mask &= signals["sentiment"].str.lower() == sentiment.lower()
86
+ if keyword:
87
+ mask &= signals["name"].str.contains(keyword, case=False, na=False)
88
+ results = signals[mask][["signal_id", "name", "domain", "subdomain",
89
+ "sentiment", "impact", "timestamp"]].head(15)
90
+ if results.empty:
91
+ return "No signals found matching the criteria."
92
+ return results.to_markdown(index=False)
93
+
94
+
95
+ @tool
96
+ def get_competitors(product_id: str) -> str:
97
+ """Get vehicles that compete with a given product.
98
+ Takes a product_id like 'prd_tesla_model_y_2024' and returns competing vehicles."""
99
+ edges = get_df("hypergraph", "edges")
100
+ products = get_df("hypergraph", "product_index")
101
+ # Find COMPETES_WITH edges in both directions
102
+ compete = edges[edges["role"] == "COMPETES_WITH"]
103
+ as_source = compete[compete["source_id"] == product_id]["target_id"]
104
+ as_target = compete[compete["target_id"] == product_id]["source_id"]
105
+ competitor_ids = pd.concat([as_source, as_target]).unique()
106
+ if len(competitor_ids) == 0:
107
+ return f"No competitors found for {product_id}."
108
+ results = products[products["product_id"].isin(competitor_ids)]
109
+ return f"Competitors of {product_id}:\n\n{results.to_markdown(index=False)}"
110
+
111
+
112
+ @tool
113
+ def graph_stats() -> str:
114
+ """Get summary statistics of the auto-ontology hypergraph —
115
+ node counts by type, edge counts by role, signal domain distribution, etc."""
116
+ nodes = get_df("hypergraph", "nodes")
117
+ edges = get_df("hypergraph", "edges")
118
+ si = get_df("hypergraph", "signal_index")
119
+
120
+ node_counts = nodes["node_type"].value_counts().to_dict()
121
+ edge_counts = edges["role"].value_counts().to_dict()
122
+ domain_counts = si["domain"].value_counts().to_dict()
123
+ sentiment_counts = si["sentiment"].value_counts().to_dict()
124
+
125
+ lines = [
126
+ "## Hypergraph Statistics\n",
127
+ f"**Total nodes:** {len(nodes):,}",
128
+ f"**Total edges:** {len(edges):,}\n",
129
+ "### Node Types",
130
+ ]
131
+ for t, c in sorted(node_counts.items(), key=lambda x: -x[1]):
132
+ lines.append(f"- {t}: {c:,}")
133
+ lines.append("\n### Edge Roles")
134
+ for r, c in sorted(edge_counts.items(), key=lambda x: -x[1]):
135
+ lines.append(f"- {r}: {c:,}")
136
+ lines.append("\n### Signal Domains (L1)")
137
+ domain_names = {
138
+ "P": "Product", "C": "Competitive", "T": "Technology", "M": "Market",
139
+ "F": "Financial", "S": "Supply Chain", "R": "Regulatory", "ST": "Strategic",
140
+ }
141
+ for d, c in sorted(domain_counts.items(), key=lambda x: -x[1]):
142
+ lines.append(f"- {d} ({domain_names.get(d, d)}): {c:,}")
143
+ lines.append("\n### Signal Sentiment")
144
+ for s, c in sorted(sentiment_counts.items(), key=lambda x: -x[1]):
145
+ lines.append(f"- {s}: {c:,}")
146
+ return "\n".join(lines)
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Build Strands Agent
151
+ # ---------------------------------------------------------------------------
152
+
153
+ SYSTEM_PROMPT = """\
154
+ You are an automotive market intelligence analyst with access to the Auto-Ontology \
155
+ hypergraph — 176K nodes and 537K edges connecting 94,671 market signals to 1,261 vehicles.
156
+
157
+ The data was extracted from Common Crawl and resolved against the NHTSA vPIC registry.
158
+
159
+ Use your tools to search products, browse signals, find competitors, and get graph stats. \
160
+ When answering, cite specific data from the tools. Be concise and analytical.
161
+
162
+ Signal domains: P (Product), T (Technology), C (Competitive), F (Financial), \
163
+ S (Supply Chain), R (Regulatory), M (Market), ST (Strategic).
164
+
165
+ Sentiments: bullish, bearish, neutral, mixed.
166
+ """
167
+
168
+
169
+ def _build_agent():
170
+ """Build the Strands agent with HF Inference API."""
171
+ hf_token = os.environ.get("HF_TOKEN", "")
172
+ model = OpenAIModel(
173
+ client_args={
174
+ "base_url": "https://api-inference.huggingface.co/v1/",
175
+ "api_key": hf_token,
176
+ },
177
+ model_id="Qwen/Qwen2.5-72B-Instruct",
178
+ )
179
+ return Agent(
180
+ model=model,
181
+ tools=[search_products, browse_signals, get_competitors, graph_stats],
182
+ system_prompt=SYSTEM_PROMPT,
183
+ )
184
+
185
+
186
+ _agent = None
187
+
188
+
189
+ def get_agent():
190
+ global _agent
191
+ if _agent is None:
192
+ _agent = _build_agent()
193
+ return _agent
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Chat handler
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ def chat_fn(message: str, history: list[dict]) -> str:
202
+ """Handle a chat message using the Strands agent."""
203
+ try:
204
+ agent = get_agent()
205
+ result = agent(message)
206
+ return str(result)
207
+ except Exception as e:
208
+ return f"Error: {e}\n\nMake sure the HF_TOKEN secret is configured in Space settings."
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # Pipeline narrative HTML
213
+ # ---------------------------------------------------------------------------
214
+
215
+ PIPELINE_HTML_PATH = Path(__file__).parent / "pipeline.html"
216
+
217
+
218
+ def load_pipeline_html() -> str:
219
+ if PIPELINE_HTML_PATH.exists():
220
+ html = PIPELINE_HTML_PATH.read_text()
221
+ # Wrap in iframe for isolation
222
+ import base64
223
+ encoded = base64.b64encode(html.encode()).decode()
224
+ return f'<iframe src="data:text/html;base64,{encoded}" width="100%" height="900" style="border:none; border-radius:12px;"></iframe>'
225
+ return "<p>Pipeline narrative not found. Check pipeline.html.</p>"
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Gradio App
230
+ # ---------------------------------------------------------------------------
231
+
232
+ DESCRIPTION = """\
233
+ # Auto-Ontology — Automotive Market Intelligence
234
+
235
+ Explore a hypergraph of **94,671 market signals** connected to **1,261 vehicles** \
236
+ from the NHTSA vPIC registry. Built from Common Crawl data using an AWS pipeline \
237
+ with NuExtract structured extraction and vPIC entity resolution.
238
+ """
239
+
240
+ with gr.Blocks(
241
+ title="Auto-Ontology",
242
+ theme=gr.themes.Base(
243
+ primary_hue="indigo",
244
+ secondary_hue="emerald",
245
+ neutral_hue="slate",
246
+ ),
247
+ ) as demo:
248
+ gr.Markdown(DESCRIPTION)
249
+
250
+ with gr.Tabs():
251
+ with gr.Tab("The Pipeline"):
252
+ gr.HTML(load_pipeline_html())
253
+
254
+ with gr.Tab("Ask the Ontology"):
255
+ gr.Markdown(
256
+ "Chat with a **Strands Agent** that can search products, "
257
+ "browse market signals, find competitors, and query graph statistics."
258
+ )
259
+ gr.ChatInterface(
260
+ fn=chat_fn,
261
+ type="messages",
262
+ examples=[
263
+ "What are the graph statistics?",
264
+ "Search for Tesla vehicles in the dataset",
265
+ "Show me bearish signals in the technology domain",
266
+ "What competes with the Tesla Model Y 2024?",
267
+ "Find signals about battery technology",
268
+ ],
269
+ )
270
+
271
+
272
+ if __name__ == "__main__":
273
+ demo.launch()
pipeline.html ADDED
@@ -0,0 +1,1072 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Auto-Ontology Pipeline</title>
7
+ <script src="https://d3js.org/d3.v7.min.js"></script>
8
+ <style>
9
+ :root {
10
+ --bg: #0f172a;
11
+ --surface: #1e293b;
12
+ --border: #334155;
13
+ --text: #e2e8f0;
14
+ --muted: #94a3b8;
15
+ --accent: #818cf8;
16
+ --accent2: #34d399;
17
+ --accent3: #f472b6;
18
+ --accent4: #fbbf24;
19
+ --bearish: #ef4444;
20
+ --bullish: #22c55e;
21
+ --neutral: #64748b;
22
+ }
23
+ * { margin: 0; padding: 0; box-sizing: border-box; }
24
+ body {
25
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ line-height: 1.7;
29
+ overflow-x: hidden;
30
+ }
31
+
32
+ /* ─── Hero ─── */
33
+ .hero {
34
+ min-height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ justify-content: center;
38
+ align-items: center;
39
+ text-align: center;
40
+ padding: 2rem;
41
+ position: relative;
42
+ overflow: hidden;
43
+ }
44
+ .hero::before {
45
+ content: '';
46
+ position: absolute;
47
+ inset: 0;
48
+ background: radial-gradient(ellipse 80% 60% at 50% 40%, rgba(99,102,241,0.15), transparent);
49
+ }
50
+ .hero h1 {
51
+ font-size: clamp(2.5rem, 6vw, 4.5rem);
52
+ font-weight: 800;
53
+ letter-spacing: -0.02em;
54
+ background: linear-gradient(135deg, #818cf8, #34d399);
55
+ -webkit-background-clip: text;
56
+ -webkit-text-fill-color: transparent;
57
+ background-clip: text;
58
+ position: relative;
59
+ }
60
+ .hero .subtitle {
61
+ font-size: clamp(1rem, 2.5vw, 1.5rem);
62
+ color: var(--muted);
63
+ margin-top: 1rem;
64
+ position: relative;
65
+ }
66
+ .hero .stats-row {
67
+ display: flex;
68
+ gap: 3rem;
69
+ margin-top: 3rem;
70
+ flex-wrap: wrap;
71
+ justify-content: center;
72
+ position: relative;
73
+ }
74
+ .stat { text-align: center; }
75
+ .stat .num {
76
+ font-size: 2.5rem;
77
+ font-weight: 700;
78
+ color: var(--accent);
79
+ font-variant-numeric: tabular-nums;
80
+ }
81
+ .stat .label { color: var(--muted); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; }
82
+ .scroll-hint {
83
+ position: absolute;
84
+ bottom: 2rem;
85
+ animation: bounce 2s infinite;
86
+ color: var(--muted);
87
+ font-size: 0.875rem;
88
+ }
89
+ @keyframes bounce {
90
+ 0%, 100% { transform: translateY(0); }
91
+ 50% { transform: translateY(8px); }
92
+ }
93
+
94
+ /* ─── Sections ─── */
95
+ .section {
96
+ min-height: 100vh;
97
+ padding: 6rem 2rem;
98
+ max-width: 1100px;
99
+ margin: 0 auto;
100
+ display: flex;
101
+ flex-direction: column;
102
+ justify-content: center;
103
+ }
104
+ .section-num {
105
+ font-size: 0.75rem;
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.15em;
108
+ color: var(--accent);
109
+ margin-bottom: 0.5rem;
110
+ }
111
+ .section h2 {
112
+ font-size: clamp(1.75rem, 4vw, 3rem);
113
+ font-weight: 700;
114
+ margin-bottom: 1rem;
115
+ }
116
+ .section .lead {
117
+ font-size: 1.125rem;
118
+ color: var(--muted);
119
+ margin-bottom: 2rem;
120
+ max-width: 700px;
121
+ }
122
+
123
+ /* ─── Cards ─── */
124
+ .card {
125
+ background: var(--surface);
126
+ border: 1px solid var(--border);
127
+ border-radius: 12px;
128
+ padding: 1.5rem;
129
+ margin-bottom: 1.5rem;
130
+ }
131
+ .card-header {
132
+ font-size: 0.75rem;
133
+ text-transform: uppercase;
134
+ letter-spacing: 0.1em;
135
+ color: var(--accent);
136
+ margin-bottom: 0.75rem;
137
+ }
138
+
139
+ /* ─── Viz containers ─── */
140
+ .viz {
141
+ background: var(--surface);
142
+ border: 1px solid var(--border);
143
+ border-radius: 12px;
144
+ padding: 1.5rem;
145
+ margin: 2rem 0;
146
+ overflow: hidden;
147
+ }
148
+ .viz svg { width: 100%; display: block; }
149
+
150
+ /* ─── Waterfall ─── */
151
+ .waterfall-container {
152
+ height: 300px;
153
+ position: relative;
154
+ overflow: hidden;
155
+ border-radius: 8px;
156
+ background: #0c1222;
157
+ }
158
+ .waterfall-url {
159
+ position: absolute;
160
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
161
+ font-size: 0.7rem;
162
+ color: var(--accent);
163
+ opacity: 0;
164
+ white-space: nowrap;
165
+ animation: fall linear infinite;
166
+ }
167
+ @keyframes fall {
168
+ 0% { opacity: 0; transform: translateY(-20px); }
169
+ 10% { opacity: 0.7; }
170
+ 90% { opacity: 0.7; }
171
+ 100% { opacity: 0; transform: translateY(320px); }
172
+ }
173
+
174
+ /* ─── Before/After ─── */
175
+ .before-after {
176
+ display: grid;
177
+ grid-template-columns: 1fr auto 1fr;
178
+ gap: 1rem;
179
+ align-items: start;
180
+ }
181
+ .arrow-col {
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ padding-top: 3rem;
186
+ font-size: 2rem;
187
+ color: var(--accent);
188
+ }
189
+ pre {
190
+ background: #0c1222;
191
+ border: 1px solid var(--border);
192
+ border-radius: 8px;
193
+ padding: 1rem;
194
+ font-size: 0.75rem;
195
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
196
+ overflow-x: auto;
197
+ line-height: 1.5;
198
+ }
199
+ pre .key { color: var(--accent); }
200
+ pre .str { color: var(--accent2); }
201
+ pre .num { color: var(--accent4); }
202
+ pre .comment { color: var(--neutral); }
203
+
204
+ /* ─── Two-col layout ─── */
205
+ .two-col {
206
+ display: grid;
207
+ grid-template-columns: 1fr 1fr;
208
+ gap: 1.5rem;
209
+ }
210
+ @media (max-width: 768px) {
211
+ .two-col, .before-after { grid-template-columns: 1fr; }
212
+ .arrow-col { transform: rotate(90deg); padding: 0; }
213
+ }
214
+
215
+ /* ─── Stats pills ─── */
216
+ .pill-row {
217
+ display: flex;
218
+ gap: 0.75rem;
219
+ flex-wrap: wrap;
220
+ margin: 1rem 0;
221
+ }
222
+ .pill {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ gap: 0.5rem;
226
+ padding: 0.5rem 1rem;
227
+ border-radius: 999px;
228
+ font-size: 0.8rem;
229
+ font-weight: 600;
230
+ border: 1px solid var(--border);
231
+ background: var(--surface);
232
+ }
233
+ .pill .dot {
234
+ width: 8px; height: 8px;
235
+ border-radius: 50%;
236
+ display: inline-block;
237
+ }
238
+
239
+ /* ─── Flow diagram ─── */
240
+ .flow-step {
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 1rem;
244
+ margin: 0.5rem 0;
245
+ }
246
+ .flow-icon {
247
+ width: 48px; height: 48px;
248
+ border-radius: 12px;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ font-size: 1.5rem;
253
+ flex-shrink: 0;
254
+ }
255
+ .flow-connector {
256
+ width: 2px;
257
+ height: 24px;
258
+ background: var(--border);
259
+ margin-left: 23px;
260
+ }
261
+
262
+ /* ─── Persona cards ─── */
263
+ .persona-grid {
264
+ display: grid;
265
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
266
+ gap: 1rem;
267
+ margin-top: 1.5rem;
268
+ }
269
+ .persona {
270
+ background: var(--surface);
271
+ border: 1px solid var(--border);
272
+ border-radius: 12px;
273
+ padding: 1.25rem;
274
+ transition: border-color 0.2s;
275
+ }
276
+ .persona:hover { border-color: var(--accent); }
277
+ .persona .icon { font-size: 1.5rem; margin-bottom: 0.5rem; }
278
+ .persona .name { font-weight: 600; margin-bottom: 0.25rem; }
279
+ .persona .desc { font-size: 0.8rem; color: var(--muted); }
280
+
281
+ /* ─── Footer ─── */
282
+ .footer {
283
+ text-align: center;
284
+ padding: 4rem 2rem;
285
+ color: var(--muted);
286
+ font-size: 0.875rem;
287
+ }
288
+ .footer a { color: var(--accent); text-decoration: none; }
289
+
290
+ /* ─── Intersection observer fade-in ─── */
291
+ .reveal {
292
+ opacity: 0;
293
+ transform: translateY(30px);
294
+ transition: opacity 0.8s ease, transform 0.8s ease;
295
+ }
296
+ .reveal.visible {
297
+ opacity: 1;
298
+ transform: translateY(0);
299
+ }
300
+ </style>
301
+ </head>
302
+ <body>
303
+
304
+ <!-- ═══════════ HERO ═══════════ -->
305
+ <div class="hero">
306
+ <h1>Structural Intelligence</h1>
307
+ <p class="subtitle">From Common Crawl to Automotive Market Intelligence</p>
308
+ <div class="stats-row">
309
+ <div class="stat"><div class="num" data-count="176324">0</div><div class="label">Nodes</div></div>
310
+ <div class="stat"><div class="num" data-count="537747">0</div><div class="label">Edges</div></div>
311
+ <div class="stat"><div class="num" data-count="94671">0</div><div class="label">Signals</div></div>
312
+ <div class="stat"><div class="num" data-count="1261">0</div><div class="label">Vehicles</div></div>
313
+ </div>
314
+ <div class="scroll-hint">Scroll to explore the pipeline ↓</div>
315
+ </div>
316
+
317
+ <!-- ═══════════ SECTION 1: Common Crawl ═══════════ -->
318
+ <div class="section reveal" id="s1">
319
+ <div class="section-num">Step 01</div>
320
+ <h2>Common Crawl Scraping</h2>
321
+ <p class="lead">Billions of web pages archived by Common Crawl. We query the CC index for automotive domains, download WARC records, and extract clean article text.</p>
322
+
323
+ <div class="viz">
324
+ <div class="card-header">Live URL Waterfall</div>
325
+ <div class="waterfall-container" id="waterfall"></div>
326
+ </div>
327
+
328
+ <div class="two-col">
329
+ <div class="card">
330
+ <div class="card-header">The Funnel</div>
331
+ <div id="funnel-chart" style="height:200px;"></div>
332
+ </div>
333
+ <div class="card">
334
+ <div class="card-header">Sample Documents</div>
335
+ <pre><span class="comment">// Real URLs from the dataset</span>
336
+ <span class="str">"autocar.co.uk/car-news/business-electric-vehicles/
337
+ hydrogen-car-dream-good-dead"</span>
338
+
339
+ <span class="str">"autocar.co.uk/car-news/business-electric-vehicles/
340
+ kia-uk-boss-calls-clarity-hybrid-sales-after-2030"</span>
341
+
342
+ <span class="str">"autocar.co.uk/car-news/business-environment-and-energy/
343
+ kia-europe-reuse-electric-car-batteries-energy-storage"</span>
344
+
345
+ <span class="comment">// Crawl: CC-MAIN-2026-04</span></pre>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <!-- ═══════════ SECTION 2: Extraction ═══════════ -->
351
+ <div class="section reveal" id="s2">
352
+ <div class="section-num">Step 02</div>
353
+ <h2>Structured Extraction</h2>
354
+ <p class="lead">NuExtract-2.0 with an ontology-enforced JSON schema transforms raw article text into structured market signals — products, events, companies, and feature-level sentiment.</p>
355
+
356
+ <div class="before-after">
357
+ <div>
358
+ <div class="card-header">Raw Article Text</div>
359
+ <pre style="min-height:280px"><span class="comment">// From autocar.co.uk</span>
360
+
361
+ H2 Mobility has announced it is
362
+ shutting 22 of its hydrogen fuel
363
+ stations in Germany, dealing a
364
+ significant blow to the hydrogen
365
+ car movement in Europe.
366
+
367
+ The company cited low demand and
368
+ high operational costs. Only about
369
+ 100 hydrogen fuel cell vehicles
370
+ are registered in Germany...</pre>
371
+ </div>
372
+ <div class="arrow-col">→</div>
373
+ <div>
374
+ <div class="card-header">NuExtract JSON Output</div>
375
+ <pre style="min-height:280px">{
376
+ <span class="key">"market_relevance"</span>: <span class="str">"high"</span>,
377
+ <span class="key">"signals"</span>: [{
378
+ <span class="key">"event_description"</span>:
379
+ <span class="str">"H2 Mobility announced shutting
380
+ 22 fuel stations in Germany"</span>,
381
+ <span class="key">"l1_domain"</span>: <span class="str">"C"</span>,
382
+ <span class="key">"sentiment"</span>: <span class="str">"bearish"</span>,
383
+ <span class="key">"impact"</span>: <span class="str">"high"</span>,
384
+ <span class="key">"confidence"</span>: <span class="str">"confirmed"</span>
385
+ }],
386
+ <span class="key">"companies"</span>: [{
387
+ <span class="key">"name"</span>: <span class="str">"H2 Mobility"</span>,
388
+ <span class="key">"role"</span>: <span class="str">"subject"</span>
389
+ }]
390
+ }</pre>
391
+ </div>
392
+ </div>
393
+
394
+ <div class="card" style="margin-top:1.5rem;">
395
+ <div class="card-header">Signal Taxonomy (8 L1 Domains)</div>
396
+ <div id="domain-chart" style="height:220px;"></div>
397
+ </div>
398
+ </div>
399
+
400
+ <!-- ═══════════ SECTION 3: Normalization ═══════════ -->
401
+ <div class="section reveal" id="s3">
402
+ <div class="section-num">Step 03</div>
403
+ <h2>Entity Resolution</h2>
404
+ <p class="lead">The "Ralph Wiggum" quality loop validates extractions against the NHTSA vPIC database. Fuzzy matching resolves messy text to canonical product IDs through up to 6 depth levels.</p>
405
+
406
+ <div class="viz">
407
+ <div class="card-header">Fuzzy Match Resolution</div>
408
+ <div id="resolution-viz" style="height:220px;"></div>
409
+ </div>
410
+
411
+ <div class="two-col">
412
+ <div class="card">
413
+ <div class="card-header">Resolution Depth Levels</div>
414
+ <div class="flow-step">
415
+ <div class="flow-icon" style="background:rgba(129,140,248,0.15); color:var(--accent);">1</div>
416
+ <div><strong>Make only</strong> <span style="color:var(--muted)">— "Tesla" → confidence 0.2</span></div>
417
+ </div>
418
+ <div class="flow-connector"></div>
419
+ <div class="flow-step">
420
+ <div class="flow-icon" style="background:rgba(129,140,248,0.2); color:var(--accent);">2</div>
421
+ <div><strong>+ Model</strong> <span style="color:var(--muted)">— "Model Y" → confidence 0.4</span></div>
422
+ </div>
423
+ <div class="flow-connector"></div>
424
+ <div class="flow-step">
425
+ <div class="flow-icon" style="background:rgba(129,140,248,0.25); color:var(--accent);">3</div>
426
+ <div><strong>+ Year</strong> <span style="color:var(--muted)">— "2024" → confidence 0.6</span></div>
427
+ </div>
428
+ <div class="flow-connector"></div>
429
+ <div class="flow-step">
430
+ <div class="flow-icon" style="background:rgba(129,140,248,0.3); color:var(--accent);">4</div>
431
+ <div><strong>+ Body Class</strong> <span style="color:var(--muted)">— "SUV" → confidence 0.7</span></div>
432
+ </div>
433
+ <div class="flow-connector"></div>
434
+ <div class="flow-step">
435
+ <div class="flow-icon" style="background:rgba(129,140,248,0.35); color:var(--accent);">5</div>
436
+ <div><strong>+ Powertrain</strong> <span style="color:var(--muted)">— "BEV" → confidence 0.8</span></div>
437
+ </div>
438
+ <div class="flow-connector"></div>
439
+ <div class="flow-step">
440
+ <div class="flow-icon" style="background:rgba(129,140,248,0.4); color:var(--accent);">6</div>
441
+ <div><strong>+ Trim</strong> <span style="color:var(--muted)">— "Long Range" → confidence 0.95</span></div>
442
+ </div>
443
+ </div>
444
+ <div class="card">
445
+ <div class="card-header">vPIC Feature Groups (160 Elements)</div>
446
+ <div class="pill-row">
447
+ <span class="pill"><span class="dot" style="background:#818cf8"></span>Powertrain</span>
448
+ <span class="pill"><span class="dot" style="background:#34d399"></span>Electrification</span>
449
+ <span class="pill"><span class="dot" style="background:#f472b6"></span>Safety (Active)</span>
450
+ <span class="pill"><span class="dot" style="background:#ef4444"></span>Safety (Passive)</span>
451
+ <span class="pill"><span class="dot" style="background:#fbbf24"></span>Body</span>
452
+ <span class="pill"><span class="dot" style="background:#38bdf8"></span>Interior</span>
453
+ <span class="pill"><span class="dot" style="background:#a78bfa"></span>Wheels &amp; Tires</span>
454
+ <span class="pill"><span class="dot" style="background:#fb923c"></span>Vehicle ID</span>
455
+ <span class="pill"><span class="dot" style="background:#64748b"></span>Specialty</span>
456
+ </div>
457
+ <pre style="margin-top:1rem"><span class="comment">// Example resolution</span>
458
+ <span class="str">"Tesla Model Y Long Range"</span>
459
+ → make: <span class="str">"Tesla"</span>
460
+ → model: <span class="str">"Model Y"</span>
461
+ → year: <span class="num">2024</span>
462
+ → body: <span class="str">"SUV"</span>
463
+ → powertrain: <span class="str">"BEV"</span>
464
+ → id: <span class="key">prd_tesla_model_y_2024</span></pre>
465
+ </div>
466
+ </div>
467
+ </div>
468
+
469
+ <!-- ═══════════ SECTION 4: Temporal Stitching ═══════════ -->
470
+ <div class="section reveal" id="s4">
471
+ <div class="section-num">Step 04</div>
472
+ <h2>Temporal Stitching</h2>
473
+ <p class="lead">Signals sharing a product are connected with NEXT edges ordered by timestamp, creating event chains that reveal how stories evolve over time.</p>
474
+
475
+ <div class="viz">
476
+ <div class="card-header">Signal Timeline — Event Chain</div>
477
+ <div id="timeline-viz" style="height:280px;"></div>
478
+ </div>
479
+
480
+ <div class="two-col">
481
+ <div class="card">
482
+ <div class="card-header">How Stitching Works</div>
483
+ <pre><span class="comment">// For each product, order signals by date</span>
484
+ <span class="comment">// Connect consecutive signals with NEXT edges</span>
485
+ <span class="comment">// Max gap: 90 days</span>
486
+
487
+ sig_001 (<span class="str">"Production starts"</span>, Jan 15)
488
+ <span class="key">─NEXT→</span>
489
+ sig_002 (<span class="str">"First deliveries"</span>, Mar 22)
490
+ <span class="key">─NEXT→</span>
491
+ sig_003 (<span class="str">"Recall issued"</span>, May 10)
492
+ <span class="key">─NEXT→</span>
493
+ sig_004 (<span class="str">"Software update"</span>, Jun 03)</pre>
494
+ </div>
495
+ <div class="card">
496
+ <div class="card-header">Temporal Edge Stats</div>
497
+ <div style="margin-top:0.5rem;">
498
+ <div class="stat" style="text-align:left; margin-bottom:1rem;">
499
+ <div class="num" style="font-size:2rem;">84,981</div>
500
+ <div class="label">NEXT edges</div>
501
+ </div>
502
+ <p style="color:var(--muted); font-size:0.875rem;">
503
+ Temporal chains let analysts track how market events cascade —
504
+ from a product launch through reviews, sales data, competitive
505
+ responses, and eventual recalls or updates.
506
+ </p>
507
+ <p style="color:var(--muted); font-size:0.875rem; margin-top:0.5rem;">
508
+ The longest chains span 4+ years of signal history for
509
+ established products like the Tesla Model 3 and Toyota RAV4.
510
+ </p>
511
+ </div>
512
+ </div>
513
+ </div>
514
+ </div>
515
+
516
+ <!-- ═══════════ SECTION 5: Hypergraph ═══════════ -->
517
+ <div class="section reveal" id="s5">
518
+ <div class="section-num">Step 05</div>
519
+ <h2>Building the Hypergraph</h2>
520
+ <p class="lead">Products, Features, Actors, Signals, Locations, and Documents are woven together through 13 typed edge roles into a unified knowledge graph — the dual-star topology.</p>
521
+
522
+ <div class="viz">
523
+ <div class="card-header">Dual-Star Graph Assembly</div>
524
+ <div id="graph-viz" style="height:400px;"></div>
525
+ </div>
526
+
527
+ <div class="two-col">
528
+ <div class="card">
529
+ <div class="card-header">Node Breakdown</div>
530
+ <div id="node-chart" style="height:200px;"></div>
531
+ </div>
532
+ <div class="card">
533
+ <div class="card-header">Edge Role Distribution</div>
534
+ <div id="edge-chart" style="height:200px;"></div>
535
+ </div>
536
+ </div>
537
+ </div>
538
+
539
+ <!-- ═══════════ SECTION 6: Structure ═══════════ -->
540
+ <div class="section reveal" id="s6">
541
+ <div class="section-num">Step 06</div>
542
+ <h2>Finding Structure</h2>
543
+ <p class="lead">Four mathematical lenses reveal patterns invisible to keyword search: competitive clusters, regime changes, signal consistency, and perception gaps.</p>
544
+
545
+ <div class="two-col">
546
+ <div class="card">
547
+ <div class="card-header">Signal Sentiment Distribution</div>
548
+ <div id="sentiment-chart" style="height:200px;"></div>
549
+ </div>
550
+ <div class="card">
551
+ <div class="card-header">Competition Clusters (COMPETES_WITH)</div>
552
+ <div id="compete-chart" style="height:200px;"></div>
553
+ </div>
554
+ </div>
555
+
556
+ <div class="persona-grid">
557
+ <div class="persona">
558
+ <div class="icon">📊</div>
559
+ <div class="name">Spectral Analysis</div>
560
+ <div class="desc">Community detection via graph Laplacian — finds competitive clusters and market segments</div>
561
+ </div>
562
+ <div class="persona">
563
+ <div class="icon">🔮</div>
564
+ <div class="name">Topology</div>
565
+ <div class="desc">Persistent homology detects regime changes — when market structure fundamentally shifts</div>
566
+ </div>
567
+ <div class="persona">
568
+ <div class="icon">🌐</div>
569
+ <div class="name">Sheaf Cohomology</div>
570
+ <div class="desc">Signal consistency across regions — where do narratives diverge?</div>
571
+ </div>
572
+ <div class="persona">
573
+ <div class="icon">🔬</div>
574
+ <div class="name">Functor Analysis</div>
575
+ <div class="desc">Feature perception vs reality gap — what the specs say vs what signals reveal</div>
576
+ </div>
577
+ </div>
578
+
579
+ <div class="card" style="margin-top:2rem;">
580
+ <div class="card-header">What Structural Intelligence Reveals</div>
581
+ <p style="color:var(--muted); margin-bottom:1rem;">Move beyond keyword search. The hypergraph reveals:</p>
582
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.75rem;">
583
+ <div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
584
+ <strong>Competitive Blind Spots</strong>
585
+ <span style="color:var(--muted); font-size:0.8rem;">Products competing on features but not recognized as rivals</span>
586
+ </div>
587
+ <div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
588
+ <strong>Signal Cascades</strong>
589
+ <span style="color:var(--muted); font-size:0.8rem;">How a supply chain disruption propagates through the market</span>
590
+ </div>
591
+ <div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
592
+ <strong>Regime Detection</strong>
593
+ <span style="color:var(--muted); font-size:0.8rem;">When market topology changes — new entrants, exits, mergers</span>
594
+ </div>
595
+ <div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
596
+ <strong>Narrative Divergence</strong>
597
+ <span style="color:var(--muted); font-size:0.8rem;">Where media sentiment conflicts with financial reality</span>
598
+ </div>
599
+ </div>
600
+ </div>
601
+ </div>
602
+
603
+ <!-- ═══════════ FOOTER ═══════════ -->
604
+ <div class="footer">
605
+ <p>Data: <a href="https://huggingface.co/datasets/cp500/auto-ontology">cp500/auto-ontology</a> · Signals extracted from <a href="https://commoncrawl.org/">Common Crawl</a> (CC-BY-4.0) · Vehicles from <a href="https://vpic.nhtsa.dot.gov/api/">NHTSA vPIC</a></p>
606
+ <p style="margin-top:0.5rem;">Built as an AWS Automotive Workshop</p>
607
+ </div>
608
+
609
+ <script>
610
+ // ─── Intersection Observer for fade-in ───
611
+ const observer = new IntersectionObserver((entries) => {
612
+ entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
613
+ }, { threshold: 0.15 });
614
+ document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
615
+
616
+ // ─── Hero counter animation ───
617
+ function animateCounters() {
618
+ document.querySelectorAll('[data-count]').forEach(el => {
619
+ const target = +el.dataset.count;
620
+ const duration = 2000;
621
+ const start = performance.now();
622
+ function tick(now) {
623
+ const t = Math.min((now - start) / duration, 1);
624
+ const ease = 1 - Math.pow(1 - t, 3);
625
+ el.textContent = Math.floor(target * ease).toLocaleString();
626
+ if (t < 1) requestAnimationFrame(tick);
627
+ }
628
+ requestAnimationFrame(tick);
629
+ });
630
+ }
631
+ setTimeout(animateCounters, 300);
632
+
633
+ // ─── Waterfall URLs ───
634
+ (function() {
635
+ const urls = [
636
+ "autocar.co.uk/car-news/electric-vehicles/...",
637
+ "motortrend.com/news/2025-model-comparison/...",
638
+ "electrek.co/2025/01/tesla-model-y-refresh/...",
639
+ "caranddriver.com/reviews/porsche-macan-ev/...",
640
+ "reuters.com/business/autos/toyota-ev-plan/...",
641
+ "insideevs.com/news/rivian-r2-production/...",
642
+ "autoblog.com/hyundai-ioniq-6-review-2025/...",
643
+ "greencarreports.com/byd-seal-us-launch/...",
644
+ "topgear.com/car-news/bmw-neue-klasse-test/...",
645
+ "thedrive.com/news/lucid-gravity-delivery/...",
646
+ "carscoops.com/mercedes-eqg-off-road-test/...",
647
+ "jalopnik.com/gm-ultium-battery-update/...",
648
+ "motor1.com/nissan-ariya-range-comparison/...",
649
+ "carbuzz.com/volkswagen-id7-tourer-review/...",
650
+ "driving.ca/ford-explorer-ev-winter-test/...",
651
+ "paultan.org/2025/proton-ev-launch-date/...",
652
+ "autoexpress.co.uk/kia-ev9-gt-review/...",
653
+ ];
654
+ const container = document.getElementById('waterfall');
655
+ if (!container) return;
656
+ let i = 0;
657
+ function spawn() {
658
+ const el = document.createElement('div');
659
+ el.className = 'waterfall-url';
660
+ el.textContent = urls[i % urls.length];
661
+ el.style.left = (Math.random() * 85) + '%';
662
+ el.style.animationDuration = (3 + Math.random() * 4) + 's';
663
+ el.style.animationDelay = '0s';
664
+ container.appendChild(el);
665
+ el.addEventListener('animationend', () => el.remove());
666
+ i++;
667
+ }
668
+ setInterval(spawn, 400);
669
+ for (let j = 0; j < 8; j++) setTimeout(spawn, j * 200);
670
+ })();
671
+
672
+ // ─── Funnel Chart (D3) ───
673
+ (function() {
674
+ const data = [
675
+ { label: "Common Crawl Pages", value: 3200000000, color: "#334155" },
676
+ { label: "Automotive Domains", value: 2400000, color: "#475569" },
677
+ { label: "High-Relevance Docs", value: 69093, color: "#818cf8" },
678
+ { label: "Extracted Signals", value: 94671, color: "#34d399" },
679
+ ];
680
+ const container = d3.select('#funnel-chart');
681
+ if (container.empty()) return;
682
+ const w = container.node().clientWidth || 400, h = 200;
683
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
684
+ const maxW = w * 0.9;
685
+ const barH = 36;
686
+ const gap = 8;
687
+ const startY = (h - (data.length * barH + (data.length - 1) * gap)) / 2;
688
+
689
+ data.forEach((d, i) => {
690
+ const ratio = i === 0 ? 1 : 0.25 + 0.75 * (1 - i / data.length);
691
+ const bw = maxW * ratio;
692
+ const x = (w - bw) / 2;
693
+ const y = startY + i * (barH + gap);
694
+ svg.append('rect').attr('x', x).attr('y', y).attr('width', bw).attr('height', barH)
695
+ .attr('rx', 6).attr('fill', d.color);
696
+ svg.append('text').attr('x', w / 2).attr('y', y + barH / 2 + 1)
697
+ .attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
698
+ .attr('fill', '#e2e8f0').attr('font-size', '12px').attr('font-weight', '600')
699
+ .text(`${d.label}: ${d.value >= 1e9 ? (d.value/1e9).toFixed(1)+'B' : d.value >= 1e6 ? (d.value/1e6).toFixed(1)+'M' : d.value.toLocaleString()}`);
700
+ });
701
+ })();
702
+
703
+ // ─── Domain Horizontal Bar Chart ───
704
+ (function() {
705
+ const data = [
706
+ { domain: "P — Product", count: 41116, color: "#818cf8" },
707
+ { domain: "C — Competitive", count: 40065, color: "#34d399" },
708
+ { domain: "T — Technology", count: 8080, color: "#f472b6" },
709
+ { domain: "M — Market", count: 3014, color: "#fbbf24" },
710
+ { domain: "F — Financial", count: 1030, color: "#38bdf8" },
711
+ { domain: "S — Supply Chain", count: 731, color: "#fb923c" },
712
+ { domain: "R — Regulatory", count: 387, color: "#a78bfa" },
713
+ { domain: "ST — Strategic", count: 248, color: "#64748b" },
714
+ ];
715
+ const container = d3.select('#domain-chart');
716
+ if (container.empty()) return;
717
+ const w = container.node().clientWidth || 500, h = 220;
718
+ const margin = { top: 10, right: 60, bottom: 10, left: 130 };
719
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
720
+
721
+ const x = d3.scaleLinear().domain([0, d3.max(data, d => d.count)]).range([0, w - margin.left - margin.right]);
722
+ const y = d3.scaleBand().domain(data.map(d => d.domain)).range([margin.top, h - margin.bottom]).padding(0.3);
723
+ const g = svg.append('g').attr('transform', `translate(${margin.left},0)`);
724
+
725
+ g.selectAll('rect').data(data).join('rect')
726
+ .attr('x', 0).attr('y', d => y(d.domain))
727
+ .attr('width', d => x(d.count)).attr('height', y.bandwidth())
728
+ .attr('rx', 4).attr('fill', d => d.color);
729
+
730
+ g.selectAll('.label').data(data).join('text')
731
+ .attr('x', -8).attr('y', d => y(d.domain) + y.bandwidth() / 2)
732
+ .attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
733
+ .attr('fill', '#e2e8f0').attr('font-size', '11px').text(d => d.domain);
734
+
735
+ g.selectAll('.count').data(data).join('text')
736
+ .attr('x', d => x(d.count) + 6).attr('y', d => y(d.domain) + y.bandwidth() / 2)
737
+ .attr('dominant-baseline', 'middle')
738
+ .attr('fill', '#94a3b8').attr('font-size', '10px').text(d => d.count.toLocaleString());
739
+ })();
740
+
741
+ // ─── Resolution Viz (animated fuzzy match) ───
742
+ (function() {
743
+ const container = d3.select('#resolution-viz');
744
+ if (container.empty()) return;
745
+ const w = container.node().clientWidth || 500, h = 220;
746
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
747
+
748
+ const steps = [
749
+ { x: 0.08, text: '"Tesla Model Y\nLong Range 2024"', color: '#94a3b8' },
750
+ { x: 0.35, text: 'Fuzzy Match\nvs vPIC', color: '#818cf8' },
751
+ { x: 0.65, text: 'Depth 6\nConf: 0.95', color: '#34d399' },
752
+ { x: 0.92, text: 'prd_tesla_\nmodel_y_2024', color: '#fbbf24' },
753
+ ];
754
+ steps.forEach((s, i) => {
755
+ const cx = s.x * w, cy = h / 2;
756
+ svg.append('circle').attr('cx', cx).attr('cy', cy).attr('r', 38)
757
+ .attr('fill', 'none').attr('stroke', s.color).attr('stroke-width', 2);
758
+ const lines = s.text.split('\n');
759
+ lines.forEach((line, li) => {
760
+ svg.append('text').attr('x', cx).attr('y', cy - 6 + li * 16)
761
+ .attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
762
+ .attr('fill', s.color).attr('font-size', '10px').attr('font-weight', '600')
763
+ .text(line);
764
+ });
765
+ if (i < steps.length - 1) {
766
+ const nx = steps[i + 1].x * w;
767
+ svg.append('line')
768
+ .attr('x1', cx + 42).attr('y1', cy).attr('x2', nx - 42).attr('y2', cy)
769
+ .attr('stroke', '#334155').attr('stroke-width', 2).attr('marker-end', 'url(#arrow)');
770
+ }
771
+ });
772
+ svg.append('defs').append('marker').attr('id', 'arrow').attr('viewBox', '0 0 10 10')
773
+ .attr('refX', 9).attr('refY', 5).attr('markerWidth', 6).attr('markerHeight', 6)
774
+ .attr('orient', 'auto-start-reverse')
775
+ .append('path').attr('d', 'M 0 0 L 10 5 L 0 10 z').attr('fill', '#334155');
776
+ })();
777
+
778
+ // ─── Timeline Viz ───
779
+ (function() {
780
+ const container = d3.select('#timeline-viz');
781
+ if (container.empty()) return;
782
+ const w = container.node().clientWidth || 500, h = 280;
783
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
784
+
785
+ const events = [
786
+ { date: "2024-01", label: "Production\nannouncement", sentiment: "bullish" },
787
+ { date: "2024-04", label: "Factory\ngroundbreaking", sentiment: "bullish" },
788
+ { date: "2024-08", label: "Pre-orders\nopen", sentiment: "bullish" },
789
+ { date: "2024-11", label: "Supply chain\ndelay reported", sentiment: "bearish" },
790
+ { date: "2025-02", label: "First deliveries\nbegin", sentiment: "bullish" },
791
+ { date: "2025-06", label: "OTA update\nexpands range", sentiment: "bullish" },
792
+ { date: "2025-09", label: "Recall for\nbrake sensor", sentiment: "bearish" },
793
+ ];
794
+ const colors = { bullish: '#22c55e', bearish: '#ef4444', neutral: '#64748b' };
795
+ const margin = { left: 40, right: 40, top: 40, bottom: 60 };
796
+ const x = d3.scaleLinear().domain([0, events.length - 1]).range([margin.left, w - margin.right]);
797
+ const cy = h / 2 - 10;
798
+
799
+ // Timeline line
800
+ svg.append('line').attr('x1', margin.left).attr('y1', cy).attr('x2', w - margin.right).attr('y2', cy)
801
+ .attr('stroke', '#334155').attr('stroke-width', 2);
802
+
803
+ // NEXT edge labels
804
+ for (let i = 0; i < events.length - 1; i++) {
805
+ const mx = (x(i) + x(i + 1)) / 2;
806
+ svg.append('text').attr('x', mx).attr('y', cy - 24)
807
+ .attr('text-anchor', 'middle').attr('fill', '#818cf8').attr('font-size', '9px').attr('font-weight', '600')
808
+ .text('NEXT');
809
+ svg.append('line').attr('x1', x(i) + 10).attr('y1', cy).attr('x2', x(i + 1) - 10).attr('y2', cy)
810
+ .attr('stroke', '#818cf8').attr('stroke-width', 1.5)
811
+ .attr('stroke-dasharray', '4,3');
812
+ }
813
+
814
+ // Event nodes
815
+ events.forEach((ev, i) => {
816
+ const cx2 = x(i);
817
+ svg.append('circle').attr('cx', cx2).attr('cy', cy).attr('r', 8)
818
+ .attr('fill', colors[ev.sentiment]).attr('stroke', '#0f172a').attr('stroke-width', 2);
819
+ const lines = ev.label.split('\n');
820
+ lines.forEach((line, li) => {
821
+ svg.append('text').attr('x', cx2).attr('y', cy + 24 + li * 14)
822
+ .attr('text-anchor', 'middle').attr('fill', '#e2e8f0').attr('font-size', '9px')
823
+ .text(line);
824
+ });
825
+ svg.append('text').attr('x', cx2).attr('y', cy + 24 + lines.length * 14 + 4)
826
+ .attr('text-anchor', 'middle').attr('fill', '#64748b').attr('font-size', '8px')
827
+ .text(ev.date);
828
+ });
829
+
830
+ // Legend
831
+ const legend = svg.append('g').attr('transform', `translate(${margin.left}, ${h - 20})`);
832
+ [['bullish', 'Bullish'], ['bearish', 'Bearish']].forEach(([k, label], i) => {
833
+ legend.append('circle').attr('cx', i * 90).attr('cy', 0).attr('r', 5).attr('fill', colors[k]);
834
+ legend.append('text').attr('x', i * 90 + 10).attr('y', 1)
835
+ .attr('dominant-baseline', 'middle').attr('fill', '#94a3b8').attr('font-size', '10px').text(label);
836
+ });
837
+ })();
838
+
839
+ // ─── Graph Force Simulation (Dual-Star) ───
840
+ (function() {
841
+ const container = d3.select('#graph-viz');
842
+ if (container.empty()) return;
843
+ const w = container.node().clientWidth || 600, h = 400;
844
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
845
+
846
+ const types = [
847
+ { type: 'Product', color: '#818cf8', count: 8, r: 10 },
848
+ { type: 'Signal', color: '#34d399', count: 30, r: 4 },
849
+ { type: 'Feature', color: '#f472b6', count: 12, r: 6 },
850
+ { type: 'Actor', color: '#fbbf24', count: 10, r: 7 },
851
+ { type: 'Document', color: '#64748b', count: 15, r: 3 },
852
+ ];
853
+
854
+ const nodes = [];
855
+ const links = [];
856
+ types.forEach(t => {
857
+ for (let i = 0; i < t.count; i++) {
858
+ nodes.push({ id: `${t.type}_${i}`, type: t.type, color: t.color, r: t.r });
859
+ }
860
+ });
861
+
862
+ // Connect signals to products (ABOUT_PRODUCT)
863
+ const products = nodes.filter(n => n.type === 'Product');
864
+ const signals = nodes.filter(n => n.type === 'Signal');
865
+ const features = nodes.filter(n => n.type === 'Feature');
866
+ const actors = nodes.filter(n => n.type === 'Actor');
867
+ const docs = nodes.filter(n => n.type === 'Document');
868
+
869
+ signals.forEach(s => {
870
+ const p = products[Math.floor(Math.random() * products.length)];
871
+ links.push({ source: s.id, target: p.id, color: '#818cf840' });
872
+ if (Math.random() > 0.5) {
873
+ const d = docs[Math.floor(Math.random() * docs.length)];
874
+ links.push({ source: d.id, target: s.id, color: '#64748b30' });
875
+ }
876
+ });
877
+ products.forEach(p => {
878
+ const nf = 2 + Math.floor(Math.random() * 3);
879
+ for (let i = 0; i < nf; i++) {
880
+ const f = features[Math.floor(Math.random() * features.length)];
881
+ links.push({ source: p.id, target: f.id, color: '#f472b640' });
882
+ }
883
+ const a = actors[Math.floor(Math.random() * actors.length)];
884
+ links.push({ source: p.id, target: a.id, color: '#fbbf2440' });
885
+ });
886
+
887
+ const sim = d3.forceSimulation(nodes)
888
+ .force('link', d3.forceLink(links).id(d => d.id).distance(50))
889
+ .force('charge', d3.forceManyBody().strength(-40))
890
+ .force('center', d3.forceCenter(w / 2, h / 2))
891
+ .force('collision', d3.forceCollide().radius(d => d.r + 2));
892
+
893
+ const link = svg.append('g').selectAll('line').data(links).join('line')
894
+ .attr('stroke', d => d.color).attr('stroke-width', 1);
895
+ const node = svg.append('g').selectAll('circle').data(nodes).join('circle')
896
+ .attr('r', d => d.r).attr('fill', d => d.color).attr('stroke', '#0f172a').attr('stroke-width', 1);
897
+
898
+ node.append('title').text(d => d.type);
899
+
900
+ sim.on('tick', () => {
901
+ link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
902
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
903
+ node.attr('cx', d => d.x).attr('cy', d => d.y);
904
+ });
905
+
906
+ // Legend
907
+ const lg = svg.append('g').attr('transform', `translate(16, ${h - 80})`);
908
+ types.forEach((t, i) => {
909
+ lg.append('circle').attr('cx', 0).attr('cy', i * 16).attr('r', 5).attr('fill', t.color);
910
+ lg.append('text').attr('x', 12).attr('y', i * 16 + 1)
911
+ .attr('dominant-baseline', 'middle').attr('fill', '#94a3b8').attr('font-size', '10px')
912
+ .text(t.type);
913
+ });
914
+ })();
915
+
916
+ // ─── Node Type Donut ───
917
+ (function() {
918
+ const data = [
919
+ { type: 'Signal', count: 94671, color: '#34d399' },
920
+ { type: 'Document', count: 69093, color: '#64748b' },
921
+ { type: 'Actor', count: 11189, color: '#fbbf24' },
922
+ { type: 'Product', count: 1261, color: '#818cf8' },
923
+ { type: 'Feature', count: 110, color: '#f472b6' },
924
+ ];
925
+ const container = d3.select('#node-chart');
926
+ if (container.empty()) return;
927
+ const w = container.node().clientWidth || 300, h = 200;
928
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
929
+ const r = Math.min(w, h) / 2 - 20;
930
+ const g = svg.append('g').attr('transform', `translate(${w/2 - 40}, ${h/2})`);
931
+
932
+ const pie = d3.pie().value(d => d.count).sort(null);
933
+ const arc = d3.arc().innerRadius(r * 0.55).outerRadius(r);
934
+
935
+ g.selectAll('path').data(pie(data)).join('path')
936
+ .attr('d', arc).attr('fill', d => d.data.color).attr('stroke', '#1e293b').attr('stroke-width', 2);
937
+
938
+ // Legend on right
939
+ const lg = svg.append('g').attr('transform', `translate(${w/2 + r - 20}, ${h/2 - data.length * 9})`);
940
+ data.forEach((d, i) => {
941
+ lg.append('circle').attr('cx', 0).attr('cy', i * 20).attr('r', 4).attr('fill', d.color);
942
+ lg.append('text').attr('x', 10).attr('y', i * 20 + 1)
943
+ .attr('dominant-baseline', 'middle').attr('fill', '#e2e8f0').attr('font-size', '10px')
944
+ .text(`${d.type} (${d.count.toLocaleString()})`);
945
+ });
946
+ })();
947
+
948
+ // ─── Edge Role Bar Chart ───
949
+ (function() {
950
+ const data = [
951
+ { role: 'AFFECTS', count: 186696 },
952
+ { role: 'SOURCES', count: 94671 },
953
+ { role: 'NEXT', count: 84981 },
954
+ { role: 'EMITS', count: 61151 },
955
+ { role: 'ABOUT_PRODUCT', count: 55051 },
956
+ { role: 'HAS_FEATURE', count: 22521 },
957
+ { role: 'COMPETES_WITH', count: 17835 },
958
+ { role: 'ABOUT_FEATURE', count: 12297 },
959
+ ];
960
+ const container = d3.select('#edge-chart');
961
+ if (container.empty()) return;
962
+ const w = container.node().clientWidth || 300, h = 200;
963
+ const margin = { top: 5, right: 50, bottom: 5, left: 100 };
964
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
965
+
966
+ const x = d3.scaleLinear().domain([0, d3.max(data, d => d.count)]).range([0, w - margin.left - margin.right]);
967
+ const y = d3.scaleBand().domain(data.map(d => d.role)).range([margin.top, h - margin.bottom]).padding(0.25);
968
+ const g = svg.append('g').attr('transform', `translate(${margin.left},0)`);
969
+
970
+ const colorScale = d3.scaleOrdinal().domain(data.map(d => d.role))
971
+ .range(['#818cf8','#34d399','#f472b6','#fbbf24','#38bdf8','#fb923c','#a78bfa','#64748b']);
972
+
973
+ g.selectAll('rect').data(data).join('rect')
974
+ .attr('x', 0).attr('y', d => y(d.role))
975
+ .attr('width', d => x(d.count)).attr('height', y.bandwidth())
976
+ .attr('rx', 3).attr('fill', d => colorScale(d.role));
977
+
978
+ g.selectAll('.lbl').data(data).join('text')
979
+ .attr('x', -6).attr('y', d => y(d.role) + y.bandwidth() / 2)
980
+ .attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
981
+ .attr('fill', '#e2e8f0').attr('font-size', '9px').text(d => d.role);
982
+
983
+ g.selectAll('.cnt').data(data).join('text')
984
+ .attr('x', d => x(d.count) + 4).attr('y', d => y(d.role) + y.bandwidth() / 2)
985
+ .attr('dominant-baseline', 'middle')
986
+ .attr('fill', '#94a3b8').attr('font-size', '8px').text(d => (d.count / 1000).toFixed(0) + 'K');
987
+ })();
988
+
989
+ // ─── Sentiment Donut ───
990
+ (function() {
991
+ const data = [
992
+ { label: 'Neutral', count: 72816, color: '#64748b' },
993
+ { label: 'Bullish', count: 13902, color: '#22c55e' },
994
+ { label: 'Bearish', count: 4776, color: '#ef4444' },
995
+ { label: 'Mixed', count: 3177, color: '#fbbf24' },
996
+ ];
997
+ const container = d3.select('#sentiment-chart');
998
+ if (container.empty()) return;
999
+ const w = container.node().clientWidth || 300, h = 200;
1000
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
1001
+ const r = Math.min(w, h) / 2 - 20;
1002
+ const g = svg.append('g').attr('transform', `translate(${w/2 - 30}, ${h/2})`);
1003
+
1004
+ const pie = d3.pie().value(d => d.count).sort(null);
1005
+ const arc = d3.arc().innerRadius(r * 0.55).outerRadius(r);
1006
+
1007
+ g.selectAll('path').data(pie(data)).join('path')
1008
+ .attr('d', arc).attr('fill', d => d.data.color).attr('stroke', '#1e293b').attr('stroke-width', 2);
1009
+
1010
+ g.append('text').attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
1011
+ .attr('fill', '#e2e8f0').attr('font-size', '18px').attr('font-weight', '700').text('94.7K');
1012
+ g.append('text').attr('text-anchor', 'middle').attr('y', 18)
1013
+ .attr('fill', '#94a3b8').attr('font-size', '9px').text('signals');
1014
+
1015
+ const lg = svg.append('g').attr('transform', `translate(${w/2 + r - 10}, ${h/2 - data.length * 10})`);
1016
+ data.forEach((d, i) => {
1017
+ lg.append('circle').attr('cx', 0).attr('cy', i * 22).attr('r', 4).attr('fill', d.color);
1018
+ lg.append('text').attr('x', 10).attr('y', i * 22 + 1)
1019
+ .attr('dominant-baseline', 'middle').attr('fill', '#e2e8f0').attr('font-size', '10px')
1020
+ .text(`${d.label} (${(d.count/1000).toFixed(1)}K)`);
1021
+ });
1022
+ })();
1023
+
1024
+ // ─── Competition Clusters Mini Graph ───
1025
+ (function() {
1026
+ const container = d3.select('#compete-chart');
1027
+ if (container.empty()) return;
1028
+ const w = container.node().clientWidth || 300, h = 200;
1029
+ const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
1030
+
1031
+ const clusters = [
1032
+ { name: 'Electric SUV', vehicles: ['Tesla Y', 'ID.4', 'Ioniq 5', 'EV6', 'Ariya'], x: 0.3, y: 0.4 },
1033
+ { name: 'Luxury EV', vehicles: ['Model S', 'EQS', 'i7', 'Air'], x: 0.7, y: 0.35 },
1034
+ { name: 'Pickup', vehicles: ['F-150L', 'Cybertruck', 'R1T', 'Silverado'], x: 0.5, y: 0.75 },
1035
+ ];
1036
+ const colors = ['#818cf8', '#34d399', '#fbbf24'];
1037
+
1038
+ clusters.forEach((c, ci) => {
1039
+ const cx = c.x * w, cy = c.y * h;
1040
+ // Cluster background
1041
+ svg.append('circle').attr('cx', cx).attr('cy', cy)
1042
+ .attr('r', 55).attr('fill', colors[ci]).attr('fill-opacity', 0.07)
1043
+ .attr('stroke', colors[ci]).attr('stroke-opacity', 0.3).attr('stroke-dasharray', '4,3');
1044
+
1045
+ // Vehicle nodes
1046
+ c.vehicles.forEach((v, vi) => {
1047
+ const angle = (vi / c.vehicles.length) * Math.PI * 2 - Math.PI / 2;
1048
+ const vx = cx + Math.cos(angle) * 30;
1049
+ const vy = cy + Math.sin(angle) * 30;
1050
+ svg.append('circle').attr('cx', vx).attr('cy', vy).attr('r', 4)
1051
+ .attr('fill', colors[ci]).attr('stroke', '#0f172a').attr('stroke-width', 1);
1052
+ // Connect to neighbors
1053
+ if (vi > 0) {
1054
+ const pa = ((vi - 1) / c.vehicles.length) * Math.PI * 2 - Math.PI / 2;
1055
+ const px = cx + Math.cos(pa) * 30, py = cy + Math.sin(pa) * 30;
1056
+ svg.append('line').attr('x1', px).attr('y1', py).attr('x2', vx).attr('y2', vy)
1057
+ .attr('stroke', colors[ci]).attr('stroke-opacity', 0.3).attr('stroke-width', 1);
1058
+ }
1059
+ });
1060
+ // Label
1061
+ svg.append('text').attr('x', cx).attr('y', cy - 50)
1062
+ .attr('text-anchor', 'middle').attr('fill', colors[ci]).attr('font-size', '10px').attr('font-weight', '600')
1063
+ .text(c.name);
1064
+ });
1065
+
1066
+ svg.append('text').attr('x', w / 2).attr('y', h - 8)
1067
+ .attr('text-anchor', 'middle').attr('fill', '#64748b').attr('font-size', '9px')
1068
+ .text('17,835 COMPETES_WITH edges');
1069
+ })();
1070
+ </script>
1071
+ </body>
1072
+ </html>
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=5.0
2
+ strands-agents
3
+ strands-agents-tools
4
+ datasets
5
+ pandas
6
+ pyarrow
7
+ openai