Spaces:
Running
Running
mike boone commited on
Commit ·
faddd50
1
Parent(s): 667cd22
fix: improve specialized dataset dimensions
Browse files- DEMOPREP_SELLING_STATUS.md +123 -0
- demoprep_app/dataset/generators/template_generator.py +258 -8
- demoprep_app/pipeline/dataset_first.py +12 -3
- demoprep_app/scenario/families.py +14 -0
- docs/thoughtspot_quality_reporting_source.md +29 -0
- tests/e2e_quality.py +83 -7
- tests/test_dataset_first_builders.py +110 -0
DEMOPREP_SELLING_STATUS.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DemoPrep Status Brief
|
| 2 |
+
|
| 3 |
+
## Purpose
|
| 4 |
+
|
| 5 |
+
DemoPrep is a demo-generation application built to help sales and solution teams create relevant, live analytics demos quickly. Given a company and a use case, the application researches the business context, builds a realistic analytics dataset, deploys it to Snowflake, creates a ThoughtSpot model, and generates a liveboard that can be used in customer conversations.
|
| 6 |
+
|
| 7 |
+
The goal is simple: reduce the time and friction required to create credible, company-specific demos, while improving demo quality and consistency across the field.
|
| 8 |
+
|
| 9 |
+
## What We Built
|
| 10 |
+
|
| 11 |
+
DemoPrep now supports an end-to-end workflow from company input to live ThoughtSpot assets:
|
| 12 |
+
|
| 13 |
+
1. **Company and use-case intake**
|
| 14 |
+
Users provide a target company, select a vertical / line / function, or enter a custom business context.
|
| 15 |
+
|
| 16 |
+
2. **Research and context generation**
|
| 17 |
+
The system gathers business context and uses it to shape the demo narrative, metrics, dimensions, and liveboard story.
|
| 18 |
+
|
| 19 |
+
3. **Dataset-first generation**
|
| 20 |
+
We moved away from purely schema-first synthetic data generation. The current approach builds coherent business datasets first, then derives the schema and DDL from that dataset. This has materially improved realism and reduced impossible metric combinations.
|
| 21 |
+
|
| 22 |
+
4. **Snowflake deployment**
|
| 23 |
+
DemoPrep creates the schema and loads populated tables into Snowflake.
|
| 24 |
+
|
| 25 |
+
5. **ThoughtSpot deployment**
|
| 26 |
+
The application creates the connection, tables, joins, model, and liveboard. The liveboard path uses MCP creation followed by TML post-processing for layout and polish.
|
| 27 |
+
|
| 28 |
+
6. **Quality test harness**
|
| 29 |
+
We built an end-to-end Playwright quality suite that submits demo requests, waits for the pipeline, resolves the exact ThoughtSpot model/liveboard, samples Snowflake data, and grades both data quality and liveboard quality with an AI rubric.
|
| 30 |
+
|
| 31 |
+
## Current Status
|
| 32 |
+
|
| 33 |
+
The application is now consistently completing end-to-end runs in local testing, with meaningful improvements in data quality.
|
| 34 |
+
|
| 35 |
+
Recent local six-pack result:
|
| 36 |
+
|
| 37 |
+
| Test | Data | Liveboard | Total |
|
| 38 |
+
|------|------|-----------|-------|
|
| 39 |
+
| EY Professional Services | 76 | 64 | 79.0 / B |
|
| 40 |
+
| Datadog SaaS Sales | 70 | 73 | 78.2 / B |
|
| 41 |
+
| Nike Retail Sales | 80 | 73 | 83.2 / B |
|
| 42 |
+
| Delta Airline Sales | 65 | 74 | 76.0 / B |
|
| 43 |
+
| Wells Fargo Banking Marketing | 78 | 69 | 81.2 / B |
|
| 44 |
+
| Starbucks Store Operations | 72 | 58 | 75.5 / B |
|
| 45 |
+
|
| 46 |
+
Average: **78.9 / B**
|
| 47 |
+
|
| 48 |
+
This is a major improvement from earlier runs where data quality was frequently in the 40s and 50s, with common issues such as returns exceeding sales, load factors over 100%, marketing funnel metrics out of order, and generic data that did not match the target company or use case.
|
| 49 |
+
|
| 50 |
+
## What Improved
|
| 51 |
+
|
| 52 |
+
The biggest improvement is the move toward a more coherent data-generation approach:
|
| 53 |
+
|
| 54 |
+
- **Dataset-first architecture**: We now generate business-realistic datasets before deriving DDL.
|
| 55 |
+
- **Scenario families**: The system maps requests into known scenario families such as SaaS sales, retail sales, banking marketing, airline operations, store operations, sports venue engagement, and professional services.
|
| 56 |
+
- **Custom use case handling**: Custom prompts now influence the dataset structure more strongly. For example, EY now generates professional services data instead of incorrectly producing sports and venue data.
|
| 57 |
+
- **Better test identity**: The test runner now uses page URLs and exact run context instead of schema-prefix guessing.
|
| 58 |
+
- **More reliable diagnostics**: Logging has been improved so run identity, model selection, stage completion, and errors are easier to trace.
|
| 59 |
+
- **MCP contingency path**: If the ThoughtSpot MCP service is unavailable, DemoPrep can create a clearly marked backup liveboard path rather than silently hiding the failure.
|
| 60 |
+
|
| 61 |
+
## Why This Matters Commercially
|
| 62 |
+
|
| 63 |
+
Demo quality directly affects seller confidence and customer credibility.
|
| 64 |
+
|
| 65 |
+
Today, high-quality custom demos require manual work from people who understand the account, the industry, the data model, and ThoughtSpot. That creates a bottleneck. DemoPrep reduces that bottleneck by turning a company and business problem into a working analytics experience.
|
| 66 |
+
|
| 67 |
+
The commercial value is:
|
| 68 |
+
|
| 69 |
+
- **Faster demo creation**: Move from days or weeks of prep toward same-day demo generation.
|
| 70 |
+
- **Better account relevance**: Generate demos that reflect the prospect’s company, industry, metrics, and business questions.
|
| 71 |
+
- **Repeatable quality**: Create a structured path for demos instead of relying on ad hoc manual builds.
|
| 72 |
+
- **Field leverage**: Give more sellers and solution engineers access to credible demos without requiring every demo to be hand-built.
|
| 73 |
+
- **Higher confidence in AI demos**: The quality harness creates a feedback loop so we can measure and improve the demos instead of relying on subjective inspection.
|
| 74 |
+
|
| 75 |
+
## Remaining Gaps
|
| 76 |
+
|
| 77 |
+
The application is not finished, but the core value is now visible.
|
| 78 |
+
|
| 79 |
+
Current gaps:
|
| 80 |
+
|
| 81 |
+
- **Liveboard quality trails data quality**: Data scores are now often B-level, while liveboards still need better narrative layout, stronger KPI selection, and more consistent visual variety.
|
| 82 |
+
- **Some scenarios are still generic**: Certain industry/function combinations need deeper specialization to feel truly customer-specific.
|
| 83 |
+
- **Custom extraction needs hardening**: We improved this significantly, but custom prompts still need more testing across unusual industries and ambiguous company contexts.
|
| 84 |
+
- **Operational resilience**: ThoughtSpot deployment and MCP availability remain external dependencies. We now handle some failures better, but this should continue to be hardened.
|
| 85 |
+
- **Packaging and ownership**: To sell or scale this internally, we need clearer ownership, deployment expectations, support model, and roadmap.
|
| 86 |
+
|
| 87 |
+
## Call To Action
|
| 88 |
+
|
| 89 |
+
We are asking for alignment on whether DemoPrep should move from a sprint prototype into a supported internal product motion.
|
| 90 |
+
|
| 91 |
+
Specific asks:
|
| 92 |
+
|
| 93 |
+
1. **Executive sponsorship**
|
| 94 |
+
Confirm that rapid AI-generated demo creation is a priority worth operationalizing.
|
| 95 |
+
|
| 96 |
+
2. **Pilot group**
|
| 97 |
+
Identify a small group of sellers and solution engineers to use DemoPrep on real opportunities and provide structured feedback.
|
| 98 |
+
|
| 99 |
+
3. **Quality bar**
|
| 100 |
+
Agree on what “demo-ready” means. Proposed starting bar: pipeline completion, data score 75+, liveboard score 70+, and no critical business-logic violations.
|
| 101 |
+
|
| 102 |
+
4. **Scenario prioritization**
|
| 103 |
+
Pick the highest-value vertical/function combinations to harden first based on pipeline impact.
|
| 104 |
+
|
| 105 |
+
5. **Production path**
|
| 106 |
+
Decide whether this should remain a specialist tool, become an internal field application, or eventually become a customer-facing/productized capability.
|
| 107 |
+
|
| 108 |
+
## Recommended Follow-Up
|
| 109 |
+
|
| 110 |
+
After the meeting, the recommended next steps are:
|
| 111 |
+
|
| 112 |
+
- Run a structured pilot with 5-10 field users.
|
| 113 |
+
- Select 10 priority demo scenarios and harden them to a consistent quality bar.
|
| 114 |
+
- Improve liveboard generation and post-processing so visual storytelling catches up to dataset quality.
|
| 115 |
+
- Add a dashboard for quality trends, failure reasons, scenario coverage, and user feedback.
|
| 116 |
+
- Define ownership for deployment, monitoring, support, and roadmap.
|
| 117 |
+
- Prepare a short internal demo showing before/after quality improvements and a live end-to-end generation.
|
| 118 |
+
|
| 119 |
+
## Bottom Line
|
| 120 |
+
|
| 121 |
+
DemoPrep is now showing the shape of a valuable internal sales enablement platform. It can generate complete ThoughtSpot demos from company and use-case input, and recent work has materially improved data quality and pipeline reliability.
|
| 122 |
+
|
| 123 |
+
The next decision is whether we want to treat this as an experiment, or invest enough to make it a reliable tool for the field.
|
demoprep_app/dataset/generators/template_generator.py
CHANGED
|
@@ -183,6 +183,41 @@ class TemplateDatasetGenerator(ScenarioDatasetGenerator):
|
|
| 183 |
"TEST_DRIVES": self._whole(rng, vehicles, max(vehicles + 1, vehicles * 5), 1.0),
|
| 184 |
}
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
if profile == "apparel_marketing":
|
| 187 |
impressions = self._whole(rng, 25_000, 1_200_000, mult)
|
| 188 |
clicks = self._bounded_child(rng, impressions, 0.012, 0.075)
|
|
@@ -690,18 +725,230 @@ def _dimension_values(name: str, company_name: str, context: str = "") -> list[s
|
|
| 690 |
context_low = (context or "").lower()
|
| 691 |
shipping_context = any(term in context_low for term in ("shipping", "shipment", "parcel", "freight", "delivery"))
|
| 692 |
life_sciences_context = any(term in context_low for term in ("life sciences", "pharma", "medical device", "therapy", "biotech", "healthcare"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
service_values = (
|
| 694 |
["Ground", "Express", "Freight", "International Priority", "Last Mile", "Returns", "E-Commerce Fulfillment", "Healthcare Logistics"]
|
| 695 |
if shipping_context
|
| 696 |
else ["API Gateway", "Web App", "Data Platform", "Identity", "Payments", "Reporting", "Observability", "Collaboration"]
|
| 697 |
)
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
)
|
| 703 |
values = {
|
| 704 |
-
"REGIONS":
|
| 705 |
"PRODUCTS": product_values,
|
| 706 |
"CUSTOMERS": ["Enterprise", "Mid-Market", "SMB", "Strategic", "Commercial", "Public Sector"],
|
| 707 |
"ACCOUNTS": ["Apex Systems", "Northstar Health", "Summit Retail", "Vertex Bank", "Beacon Logistics", "Cloudline Media"],
|
|
@@ -722,8 +969,8 @@ def _dimension_values(name: str, company_name: str, context: str = "") -> list[s
|
|
| 722 |
"BRANDS": ["Luxury", "Premium", "Select Service", "Extended Stay", "Resort", "Convention", "Lifestyle", "Boutique"],
|
| 723 |
"PRODUCT_CATEGORIES": ["Denim", "Outerwear", "Sportswear", "Dresses", "Footwear", "Accessories", "Kids", "Underwear"],
|
| 724 |
"STORES": [f"{company} Flagship", f"{company} Core", f"{company} Outlet", f"{company} Digital", f"{company} Urban", f"{company} Suburban"],
|
| 725 |
-
"WAREHOUSES":
|
| 726 |
-
"SUPPLIERS":
|
| 727 |
"ROUTES": ["Northeast Corridor", "West Coast Loop", "Central Express", "Transatlantic", "Pacific Shuttle", "Southern Line"],
|
| 728 |
"CARRIERS": ["Rapid Freight", "Prime Carrier", "ExpressLine", "Metro Delivery", "AirBridge", "Last Mile Co"],
|
| 729 |
"AIRPORTS": ["JFK", "LAX", "ORD", "DFW", "ATL", "SFO"],
|
|
@@ -734,7 +981,10 @@ def _dimension_values(name: str, company_name: str, context: str = "") -> list[s
|
|
| 734 |
"SERVICE_LINES": service_lines,
|
| 735 |
"DEPARTMENTS": ["Engineering", "Sales", "Operations", "Finance", "Support", "Marketing"],
|
| 736 |
"MATTERS": ["Contract Review", "Employment Claim", "Litigation", "Compliance Audit", "IP Filing", "Privacy Review"],
|
| 737 |
-
"PROGRAMS":
|
|
|
|
|
|
|
|
|
|
| 738 |
"TEAMS": context_values or [f"{company} Home Team", f"{company} Visiting Team", f"{company} Women's Team", f"{company} Development Team", f"{company} League Event", f"{company} Exhibition"],
|
| 739 |
"VENUES": context_values or [company, f"{company} Arena", f"{company} Theater", f"{company} Premium Club", f"{company} Event Hall", f"{company} Fan Zone"],
|
| 740 |
"EVENTS": context_values or [f"{company} Regular Season", f"{company} Premium Hospitality", f"{company} Group Sales Night", f"{company} Concert", f"{company} Theater Event", f"{company} Family Show"],
|
|
|
|
| 183 |
"TEST_DRIVES": self._whole(rng, vehicles, max(vehicles + 1, vehicles * 5), 1.0),
|
| 184 |
}
|
| 185 |
|
| 186 |
+
if profile == "automotive_supplier_sales":
|
| 187 |
+
quoted_units = self._whole(rng, 2_500, 420_000, mult)
|
| 188 |
+
award_rate = rng.uniform(0.18, 0.72)
|
| 189 |
+
awarded_units = max(0, int(quoted_units * award_rate))
|
| 190 |
+
asp = round(rng.uniform(42, 1_850), 2)
|
| 191 |
+
quoted_revenue = round(quoted_units * asp, 2)
|
| 192 |
+
awarded_revenue = round(awarded_units * asp, 2)
|
| 193 |
+
material_cost = round(awarded_revenue * rng.uniform(0.46, 0.68), 2)
|
| 194 |
+
manufacturing_cost = round(awarded_revenue * rng.uniform(0.12, 0.26), 2)
|
| 195 |
+
tooling_recovery = round(awarded_revenue * rng.uniform(0.01, 0.08), 2)
|
| 196 |
+
warranty_reserve = round(awarded_revenue * rng.uniform(0.004, 0.035), 2)
|
| 197 |
+
margin = round(awarded_revenue + tooling_recovery - material_cost - manufacturing_cost - warranty_reserve, 2)
|
| 198 |
+
backlog = round(awarded_revenue * rng.uniform(0.45, 2.8), 2)
|
| 199 |
+
on_time_launch = round(rng.uniform(72, 98), 2)
|
| 200 |
+
ppm = round(rng.uniform(8, 240), 2)
|
| 201 |
+
return {
|
| 202 |
+
"RFQS_RECEIVED": rng.randint(1, 18),
|
| 203 |
+
"QUOTED_UNITS": quoted_units,
|
| 204 |
+
"AWARDED_UNITS": awarded_units,
|
| 205 |
+
"QUOTE_WIN_RATE_PCT": round(award_rate * 100, 2),
|
| 206 |
+
"AVG_SELLING_PRICE_USD": asp,
|
| 207 |
+
"QUOTED_REVENUE_USD": quoted_revenue,
|
| 208 |
+
"AWARDED_REVENUE_USD": awarded_revenue,
|
| 209 |
+
"BACKLOG_USD": backlog,
|
| 210 |
+
"MATERIAL_COST_USD": material_cost,
|
| 211 |
+
"MANUFACTURING_COST_USD": manufacturing_cost,
|
| 212 |
+
"TOOLING_RECOVERY_USD": tooling_recovery,
|
| 213 |
+
"WARRANTY_RESERVE_USD": warranty_reserve,
|
| 214 |
+
"PROGRAM_MARGIN_USD": margin,
|
| 215 |
+
"PROGRAM_MARGIN_PCT": self._pct(margin, awarded_revenue),
|
| 216 |
+
"ON_TIME_LAUNCH_PCT": on_time_launch,
|
| 217 |
+
"DEFECT_PPM": ppm,
|
| 218 |
+
"ENGINEERING_CHANGE_ORDERS": rng.randint(0, 14),
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
if profile == "apparel_marketing":
|
| 222 |
impressions = self._whole(rng, 25_000, 1_200_000, mult)
|
| 223 |
clicks = self._bounded_child(rng, impressions, 0.012, 0.075)
|
|
|
|
| 725 |
context_low = (context or "").lower()
|
| 726 |
shipping_context = any(term in context_low for term in ("shipping", "shipment", "parcel", "freight", "delivery"))
|
| 727 |
life_sciences_context = any(term in context_low for term in ("life sciences", "pharma", "medical device", "therapy", "biotech", "healthcare"))
|
| 728 |
+
medical_device_context = any(
|
| 729 |
+
term in context_low or term in (company_name or "").lower()
|
| 730 |
+
for term in (
|
| 731 |
+
"medical device",
|
| 732 |
+
"surgical",
|
| 733 |
+
"robotic",
|
| 734 |
+
"robotics",
|
| 735 |
+
"intuitive surgical",
|
| 736 |
+
"da vinci",
|
| 737 |
+
"ion endoluminal",
|
| 738 |
+
"manufacturing operations",
|
| 739 |
+
"inventory",
|
| 740 |
+
"warehouse",
|
| 741 |
+
)
|
| 742 |
+
)
|
| 743 |
+
higher_ed_context = any(
|
| 744 |
+
term in context_low or term in (company_name or "").lower()
|
| 745 |
+
for term in (
|
| 746 |
+
"university",
|
| 747 |
+
"college",
|
| 748 |
+
"higher education",
|
| 749 |
+
"student",
|
| 750 |
+
"students",
|
| 751 |
+
"enrollment",
|
| 752 |
+
"admissions",
|
| 753 |
+
"retention",
|
| 754 |
+
"campus",
|
| 755 |
+
"wake forest",
|
| 756 |
+
)
|
| 757 |
+
)
|
| 758 |
service_values = (
|
| 759 |
["Ground", "Express", "Freight", "International Priority", "Last Mile", "Returns", "E-Commerce Fulfillment", "Healthcare Logistics"]
|
| 760 |
if shipping_context
|
| 761 |
else ["API Gateway", "Web App", "Data Platform", "Identity", "Payments", "Reporting", "Observability", "Collaboration"]
|
| 762 |
)
|
| 763 |
+
if medical_device_context:
|
| 764 |
+
product_values = [
|
| 765 |
+
"da Vinci 5 Surgical System",
|
| 766 |
+
"da Vinci Xi Surgical System",
|
| 767 |
+
"Ion Endoluminal System",
|
| 768 |
+
"da Vinci SP Surgical System",
|
| 769 |
+
"EndoWrist Stapler",
|
| 770 |
+
"SureForm Stapler",
|
| 771 |
+
"Vessel Sealer Extend",
|
| 772 |
+
"SynchroSeal Instrument",
|
| 773 |
+
"Monopolar Curved Scissors",
|
| 774 |
+
"Maryland Bipolar Forceps",
|
| 775 |
+
"Fenestrated Bipolar Forceps",
|
| 776 |
+
"Large Needle Driver",
|
| 777 |
+
"ProGrasp Forceps",
|
| 778 |
+
"Tip-Up Fenestrated Grasper",
|
| 779 |
+
"Cadiere Forceps",
|
| 780 |
+
"Mega SutureCut Needle Driver",
|
| 781 |
+
"Harmonic ACE Curved Shears",
|
| 782 |
+
"Permanent Cautery Hook",
|
| 783 |
+
"Endoscope Plus",
|
| 784 |
+
"High Definition Vision Cart",
|
| 785 |
+
"Surgeon Console",
|
| 786 |
+
"Patient Cart",
|
| 787 |
+
"Integrated Table Motion Kit",
|
| 788 |
+
"Firefly Fluorescence Imaging Kit",
|
| 789 |
+
"Ion Vision Probe",
|
| 790 |
+
"Ion Flexision Biopsy Needle",
|
| 791 |
+
"Bronchoscopy Catheter Kit",
|
| 792 |
+
"Single-Port Access Kit",
|
| 793 |
+
"General Surgery Instrument Tray",
|
| 794 |
+
"Urology Procedure Kit",
|
| 795 |
+
"Gynecology Procedure Kit",
|
| 796 |
+
"Thoracic Procedure Kit",
|
| 797 |
+
"Colorectal Procedure Kit",
|
| 798 |
+
"Sterile Draping Kit",
|
| 799 |
+
"Instrument Reprocessing Tray",
|
| 800 |
+
"Service Parts Kit",
|
| 801 |
+
]
|
| 802 |
+
elif life_sciences_context:
|
| 803 |
+
product_values = ["Oncology Therapy", "Immunology Therapy", "Surgical Device", "Orthopedic Implant", "Diagnostic Kit", "Cardiovascular Device", "Vision Care", "Consumer Health"]
|
| 804 |
+
else:
|
| 805 |
+
product_values = [f"{company} Core", f"{company} Plus", f"{company} Enterprise", f"{company} Analytics", f"{company} Services", f"{company} Mobile"]
|
| 806 |
+
region_values = (
|
| 807 |
+
["North America", "Europe", "Japan", "Asia Pacific", "Latin America", "Field Service Network", "Manufacturing Sites", "Regulatory Hold"]
|
| 808 |
+
if medical_device_context
|
| 809 |
+
else ["North Carolina", "Southeast", "Mid-Atlantic", "Northeast", "Midwest", "West Coast", "International", "Online / Hybrid"]
|
| 810 |
+
if higher_ed_context
|
| 811 |
+
else ["North America", "EMEA", "APAC", "Latin America", "Public Sector", "Enterprise East", "Enterprise West", "Digital"]
|
| 812 |
+
)
|
| 813 |
+
warehouse_values = (
|
| 814 |
+
[
|
| 815 |
+
"Sunnyvale Final Assembly",
|
| 816 |
+
"Sunnyvale Service Depot",
|
| 817 |
+
"Mexicali Manufacturing Center",
|
| 818 |
+
"Auburn Hills Logistics Hub",
|
| 819 |
+
"Atlanta Field Service Depot",
|
| 820 |
+
"Dallas Distribution Center",
|
| 821 |
+
"Memphis Fulfillment Center",
|
| 822 |
+
"Raleigh Clinical Supply Hub",
|
| 823 |
+
"Boston Northeast Service Depot",
|
| 824 |
+
"Chicago Central Distribution Center",
|
| 825 |
+
"Phoenix Southwest Depot",
|
| 826 |
+
"Los Angeles West Coast Hub",
|
| 827 |
+
"Seattle Field Service Depot",
|
| 828 |
+
"Toronto Canada Distribution Center",
|
| 829 |
+
"Amsterdam EMEA Distribution Center",
|
| 830 |
+
"Freiburg Instrument Repair Center",
|
| 831 |
+
"Dublin Regulatory Hold Center",
|
| 832 |
+
"Singapore APAC Logistics Hub",
|
| 833 |
+
"Tokyo Japan Service Depot",
|
| 834 |
+
"Seoul Korea Distribution Center",
|
| 835 |
+
"Sydney Australia Service Depot",
|
| 836 |
+
"Shanghai Regional Logistics Hub",
|
| 837 |
+
"Sao Paulo Latin America Depot",
|
| 838 |
+
"Dubai Emerging Markets Hub",
|
| 839 |
+
]
|
| 840 |
+
if medical_device_context
|
| 841 |
+
else ["Northeast DC", "Southeast DC", "Midwest DC", "Southwest DC", "West DC", "Import Hub"]
|
| 842 |
+
)
|
| 843 |
+
supplier_values = (
|
| 844 |
+
[
|
| 845 |
+
"Precision Motion Components",
|
| 846 |
+
"Endoscopic Optics Group",
|
| 847 |
+
"Sterile Packaging Partners",
|
| 848 |
+
"Robotic Arm Assemblies",
|
| 849 |
+
"Surgical Instrument Metals",
|
| 850 |
+
"Vision Sensor Technologies",
|
| 851 |
+
"Medical Cable Systems",
|
| 852 |
+
"Servo Motor Works",
|
| 853 |
+
"Printed Circuit Medical",
|
| 854 |
+
"Titanium Machining Partners",
|
| 855 |
+
"Clinical Cart Fabrication",
|
| 856 |
+
"Single-Use Components",
|
| 857 |
+
"Fluorescence Imaging Modules",
|
| 858 |
+
"Catheter Assembly Partners",
|
| 859 |
+
"Biopsy Needle Fabrication",
|
| 860 |
+
"Hospital Integration Hardware",
|
| 861 |
+
"Regulated Plastics Molding",
|
| 862 |
+
"Cleanroom Packaging Services",
|
| 863 |
+
"Optical Lens Manufacturing",
|
| 864 |
+
"Electromechanical Subsystems",
|
| 865 |
+
"Quality Inspection Services",
|
| 866 |
+
"Sterilization Services",
|
| 867 |
+
"Field Service Parts Supply",
|
| 868 |
+
"Global Freight Medical",
|
| 869 |
+
]
|
| 870 |
+
if medical_device_context
|
| 871 |
+
else ["Preferred Supply", "Global Components", "Prime Source", "North Coast", "Vertex Manufacturing", "Reliable Partners"]
|
| 872 |
+
)
|
| 873 |
+
program_values = (
|
| 874 |
+
[
|
| 875 |
+
"School of Business",
|
| 876 |
+
"Computer Science",
|
| 877 |
+
"Health & Exercise Science",
|
| 878 |
+
"Data Analytics",
|
| 879 |
+
"Education",
|
| 880 |
+
"Engineering Sciences",
|
| 881 |
+
"Economics",
|
| 882 |
+
"Biology",
|
| 883 |
+
"Psychology",
|
| 884 |
+
"Communication",
|
| 885 |
+
"Politics & International Affairs",
|
| 886 |
+
"Accountancy",
|
| 887 |
+
"Finance",
|
| 888 |
+
"Biomedical Sciences",
|
| 889 |
+
"Law",
|
| 890 |
+
"Divinity",
|
| 891 |
+
"Sustainability",
|
| 892 |
+
"Liberal Studies",
|
| 893 |
+
]
|
| 894 |
+
if higher_ed_context
|
| 895 |
+
else ["Business", "Computer Science", "Nursing", "Data Analytics", "Education", "Engineering"]
|
| 896 |
+
)
|
| 897 |
+
campus_values = (
|
| 898 |
+
[
|
| 899 |
+
"Reynolda Campus",
|
| 900 |
+
"Wake Downtown",
|
| 901 |
+
"Innovation Quarter",
|
| 902 |
+
"School of Business",
|
| 903 |
+
"School of Medicine",
|
| 904 |
+
"School of Law",
|
| 905 |
+
"Charlotte Center",
|
| 906 |
+
"Online Programs",
|
| 907 |
+
"Graduate School",
|
| 908 |
+
"Divinity School",
|
| 909 |
+
"Study Abroad",
|
| 910 |
+
"Athletics Cohort",
|
| 911 |
+
"Downtown Winston-Salem",
|
| 912 |
+
"Regional Partnerships",
|
| 913 |
+
"Executive Education",
|
| 914 |
+
"First-Year Experience",
|
| 915 |
+
"Pre-College Programs",
|
| 916 |
+
"Summer Session",
|
| 917 |
+
]
|
| 918 |
+
if higher_ed_context
|
| 919 |
+
else None
|
| 920 |
+
)
|
| 921 |
+
student_segment_values = (
|
| 922 |
+
[
|
| 923 |
+
"First-Year Undergraduate",
|
| 924 |
+
"Transfer Students",
|
| 925 |
+
"North Carolina Residents",
|
| 926 |
+
"Out-of-State Domestic",
|
| 927 |
+
"International Students",
|
| 928 |
+
"First-Generation Students",
|
| 929 |
+
"Pell Eligible Students",
|
| 930 |
+
"Honors Applicants",
|
| 931 |
+
"Graduate Professional",
|
| 932 |
+
"Online Graduate",
|
| 933 |
+
"Adult Learners",
|
| 934 |
+
"Legacy Families",
|
| 935 |
+
"Student Athletes",
|
| 936 |
+
"STEM Applicants",
|
| 937 |
+
"Business Applicants",
|
| 938 |
+
"Health Sciences Applicants",
|
| 939 |
+
"Rural NC Applicants",
|
| 940 |
+
"Urban Market Applicants",
|
| 941 |
+
]
|
| 942 |
+
if higher_ed_context
|
| 943 |
+
else None
|
| 944 |
+
)
|
| 945 |
+
term_values = (
|
| 946 |
+
["Fall Early Decision I", "Fall Early Decision II", "Regular Decision", "Spring Transfer", "Summer Session", "Graduate Fall", "Graduate Spring", "Online Rolling"]
|
| 947 |
+
if higher_ed_context
|
| 948 |
+
else None
|
| 949 |
)
|
| 950 |
values = {
|
| 951 |
+
"REGIONS": region_values,
|
| 952 |
"PRODUCTS": product_values,
|
| 953 |
"CUSTOMERS": ["Enterprise", "Mid-Market", "SMB", "Strategic", "Commercial", "Public Sector"],
|
| 954 |
"ACCOUNTS": ["Apex Systems", "Northstar Health", "Summit Retail", "Vertex Bank", "Beacon Logistics", "Cloudline Media"],
|
|
|
|
| 969 |
"BRANDS": ["Luxury", "Premium", "Select Service", "Extended Stay", "Resort", "Convention", "Lifestyle", "Boutique"],
|
| 970 |
"PRODUCT_CATEGORIES": ["Denim", "Outerwear", "Sportswear", "Dresses", "Footwear", "Accessories", "Kids", "Underwear"],
|
| 971 |
"STORES": [f"{company} Flagship", f"{company} Core", f"{company} Outlet", f"{company} Digital", f"{company} Urban", f"{company} Suburban"],
|
| 972 |
+
"WAREHOUSES": warehouse_values,
|
| 973 |
+
"SUPPLIERS": supplier_values,
|
| 974 |
"ROUTES": ["Northeast Corridor", "West Coast Loop", "Central Express", "Transatlantic", "Pacific Shuttle", "Southern Line"],
|
| 975 |
"CARRIERS": ["Rapid Freight", "Prime Carrier", "ExpressLine", "Metro Delivery", "AirBridge", "Last Mile Co"],
|
| 976 |
"AIRPORTS": ["JFK", "LAX", "ORD", "DFW", "ATL", "SFO"],
|
|
|
|
| 981 |
"SERVICE_LINES": service_lines,
|
| 982 |
"DEPARTMENTS": ["Engineering", "Sales", "Operations", "Finance", "Support", "Marketing"],
|
| 983 |
"MATTERS": ["Contract Review", "Employment Claim", "Litigation", "Compliance Audit", "IP Filing", "Privacy Review"],
|
| 984 |
+
"PROGRAMS": program_values,
|
| 985 |
+
"CAMPUSES": campus_values or ["Campus 1", "Campus 2", "Campus 3", "Campus 4", "Campus 5", "Campus 6"],
|
| 986 |
+
"STUDENT_SEGMENTS": student_segment_values or ["Student Segment 1", "Student Segment 2", "Student Segment 3", "Student Segment 4", "Student Segment 5", "Student Segment 6"],
|
| 987 |
+
"TERMS": term_values or ["Term 1", "Term 2", "Term 3", "Term 4", "Term 5", "Term 6"],
|
| 988 |
"TEAMS": context_values or [f"{company} Home Team", f"{company} Visiting Team", f"{company} Women's Team", f"{company} Development Team", f"{company} League Event", f"{company} Exhibition"],
|
| 989 |
"VENUES": context_values or [company, f"{company} Arena", f"{company} Theater", f"{company} Premium Club", f"{company} Event Hall", f"{company} Fan Zone"],
|
| 990 |
"EVENTS": context_values or [f"{company} Regular Season", f"{company} Premium Hospitality", f"{company} Group Sales Night", f"{company} Concert", f"{company} Theater Event", f"{company} Family Show"],
|
demoprep_app/pipeline/dataset_first.py
CHANGED
|
@@ -21,8 +21,14 @@ class DatasetFirstBuild:
|
|
| 21 |
ddl: str
|
| 22 |
|
| 23 |
|
| 24 |
-
def infer_scenario_type(
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
fn = (function or "").strip().lower()
|
| 27 |
|
| 28 |
if fn == "hr":
|
|
@@ -72,6 +78,8 @@ def infer_scenario_type(use_case: str, vertical: str | None = None, function: st
|
|
| 72 |
return "sales_pipeline"
|
| 73 |
if any(tok in text for tok in ("warehousing", "warehouse", "supply chain", "fulfillment", "distribution center")):
|
| 74 |
return "inventory_supply_chain"
|
|
|
|
|
|
|
| 75 |
if any(tok in text for tok in ("automotive", "vehicle", "dealer", "dealership")) and "sales" in text:
|
| 76 |
return "automotive_sales"
|
| 77 |
if any(tok in text for tok in ("healthcare provider", "healthcare providers", "healthcare payer", "healthcare payers", "hospital", "clinic")) and "sales" in text:
|
|
@@ -101,7 +109,7 @@ def build_dataset_first_demo(
|
|
| 101 |
llm_model: str | None = None,
|
| 102 |
use_llm_contract: bool = False,
|
| 103 |
) -> DatasetFirstBuild | None:
|
| 104 |
-
preliminary_type = infer_scenario_type(use_case, vertical, function)
|
| 105 |
extraction = extract_dataset_scenario(
|
| 106 |
company_name=company_name,
|
| 107 |
company_url=company_url,
|
|
@@ -117,6 +125,7 @@ def build_dataset_first_demo(
|
|
| 117 |
"retail_sales",
|
| 118 |
"shipping_sales",
|
| 119 |
"automotive_sales",
|
|
|
|
| 120 |
"hotel_finance",
|
| 121 |
"apparel_marketing",
|
| 122 |
"sales_pipeline",
|
|
|
|
| 21 |
ddl: str
|
| 22 |
|
| 23 |
|
| 24 |
+
def infer_scenario_type(
|
| 25 |
+
use_case: str,
|
| 26 |
+
vertical: str | None = None,
|
| 27 |
+
function: str | None = None,
|
| 28 |
+
company_name: str | None = None,
|
| 29 |
+
company_url: str | None = None,
|
| 30 |
+
) -> str | None:
|
| 31 |
+
text = f"{company_name or ''} {company_url or ''} {vertical or ''} {function or ''} {use_case or ''}".lower()
|
| 32 |
fn = (function or "").strip().lower()
|
| 33 |
|
| 34 |
if fn == "hr":
|
|
|
|
| 78 |
return "sales_pipeline"
|
| 79 |
if any(tok in text for tok in ("warehousing", "warehouse", "supply chain", "fulfillment", "distribution center")):
|
| 80 |
return "inventory_supply_chain"
|
| 81 |
+
if any(tok in text for tok in ("borgwarner", "automotive supplier", "auto supplier", "oem supplier", "tier 1", "tier-one", "component", "components", "powertrain", "drivetrain", "propulsion system")) and "sales" in text:
|
| 82 |
+
return "automotive_supplier_sales"
|
| 83 |
if any(tok in text for tok in ("automotive", "vehicle", "dealer", "dealership")) and "sales" in text:
|
| 84 |
return "automotive_sales"
|
| 85 |
if any(tok in text for tok in ("healthcare provider", "healthcare providers", "healthcare payer", "healthcare payers", "hospital", "clinic")) and "sales" in text:
|
|
|
|
| 109 |
llm_model: str | None = None,
|
| 110 |
use_llm_contract: bool = False,
|
| 111 |
) -> DatasetFirstBuild | None:
|
| 112 |
+
preliminary_type = infer_scenario_type(use_case, vertical, function, company_name, company_url)
|
| 113 |
extraction = extract_dataset_scenario(
|
| 114 |
company_name=company_name,
|
| 115 |
company_url=company_url,
|
|
|
|
| 125 |
"retail_sales",
|
| 126 |
"shipping_sales",
|
| 127 |
"automotive_sales",
|
| 128 |
+
"automotive_supplier_sales",
|
| 129 |
"hotel_finance",
|
| 130 |
"apparel_marketing",
|
| 131 |
"sales_pipeline",
|
demoprep_app/scenario/families.py
CHANGED
|
@@ -192,6 +192,20 @@ SCENARIO_FAMILIES: dict[str, ScenarioFamilyTemplate] = {
|
|
| 192 |
"How do retail, fleet, and lease channels compare?",
|
| 193 |
),
|
| 194 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
"logistics_route_operations": ScenarioFamilyTemplate(
|
| 196 |
"logistics_route_operations",
|
| 197 |
"LOGISTICS_ROUTES",
|
|
|
|
| 192 |
"How do retail, fleet, and lease channels compare?",
|
| 193 |
),
|
| 194 |
),
|
| 195 |
+
"automotive_supplier_sales": ScenarioFamilyTemplate(
|
| 196 |
+
"automotive_supplier_sales",
|
| 197 |
+
"SUPPLIER_PROGRAM_SALES",
|
| 198 |
+
"oem-customer-component-program-month",
|
| 199 |
+
"AWARDED_REVENUE_USD",
|
| 200 |
+
("OEM_CUSTOMERS", "COMPONENT_PROGRAMS", "VEHICLE_PLATFORMS", "PLANTS", "REGIONS"),
|
| 201 |
+
("RFQ", "Quoted", "Nominated", "SOP", "Ramp", "Production", "End Of Production"),
|
| 202 |
+
"automotive_supplier_sales",
|
| 203 |
+
(
|
| 204 |
+
"Which OEM customers and component programs drive awarded revenue?",
|
| 205 |
+
"Where are tooling, warranty, or material costs pressuring margin?",
|
| 206 |
+
"Which vehicle platforms have the strongest backlog and quote conversion?",
|
| 207 |
+
),
|
| 208 |
+
),
|
| 209 |
"logistics_route_operations": ScenarioFamilyTemplate(
|
| 210 |
"logistics_route_operations",
|
| 211 |
"LOGISTICS_ROUTES",
|
docs/thoughtspot_quality_reporting_source.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ThoughtSpot Quality Reporting Source
|
| 2 |
+
|
| 3 |
+
The DemoPrep quality reporting liveboard is:
|
| 4 |
+
|
| 5 |
+
- URL: `https://se-thoughtspot-cloud.thoughtspot.cloud/#/pinboard/85ccf9c9-2487-40b6-9f97-f9cf5207436b`
|
| 6 |
+
- Liveboard name: `demoprep testing`
|
| 7 |
+
- ThoughtSpot model: `demoprep_test_data`
|
| 8 |
+
- Model GUID: `db310fd7-b382-49d2-8398-b2f378d179f6`
|
| 9 |
+
- Physical table: Postgres `public.ts_quality_results`
|
| 10 |
+
- ThoughtSpot table GUID: `c12ee8a3-2b5d-4597-a873-eddb9ffaddc5`
|
| 11 |
+
|
| 12 |
+
Important: this reporting liveboard does **not** read from `session_logs`.
|
| 13 |
+
`session_logs` is useful for pipeline-stage diagnostics and run lineage, but deleting
|
| 14 |
+
bad rows from `session_logs` will not remove them from the quality reporting charts.
|
| 15 |
+
|
| 16 |
+
The liveboard's score charts use `ts_quality_results`, including:
|
| 17 |
+
|
| 18 |
+
- `Weekly Average Score Trend`: `average [Total Score] [Run Timestamp].weekly`
|
| 19 |
+
- `Weekly Average Data Score Trend`: `average [Data Score] [Run Timestamp].weekly`
|
| 20 |
+
- `Daily Data Score Trend`: `average [Data Score] [Run Timestamp].daily [Run Timestamp] = 'last 3 weeks'`
|
| 21 |
+
|
| 22 |
+
Cleanup workflow:
|
| 23 |
+
|
| 24 |
+
1. Inspect the liveboard TML if the source is uncertain.
|
| 25 |
+
2. Query `ts_quality_results` for the affected date window.
|
| 26 |
+
3. Delete or filter rows in `ts_quality_results`, not only `session_logs`, when the
|
| 27 |
+
reporting liveboard needs to change.
|
| 28 |
+
4. Use `session_logs` only to validate run provenance, user, stage completion, and
|
| 29 |
+
failure causes.
|
tests/e2e_quality.py
CHANGED
|
@@ -44,6 +44,7 @@ load_dotenv(Path(__file__).parent.parent / ".env")
|
|
| 44 |
BASE_URL = os.getenv("TEST_TARGET_URL", "") # may be overridden by --url flag at runtime
|
| 45 |
TEST_USER = os.getenv("TEST_USER")
|
| 46 |
TEST_PASSWORD = os.getenv("TEST_PASSWORD")
|
|
|
|
| 47 |
|
| 48 |
CONFIG_FILE = Path(__file__).parent / "quality_config.yaml"
|
| 49 |
RESULTS_DIR = Path(__file__).parent / "quality_results"
|
|
@@ -358,10 +359,61 @@ def _do_login(page: Page):
|
|
| 358 |
page.fill('input[type=text]', TEST_USER)
|
| 359 |
page.fill('input[type=password]', TEST_PASSWORD)
|
| 360 |
page.click('button:has-text("Login")')
|
|
|
|
|
|
|
| 361 |
page.wait_for_selector('button[role=tab]', timeout=90000)
|
| 362 |
page.wait_for_timeout(3000)
|
| 363 |
|
| 364 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
|
| 366 |
"""
|
| 367 |
Navigate to BASE_URL and ensure we're on the logged-in app.
|
|
@@ -375,10 +427,7 @@ def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
|
|
| 375 |
try:
|
| 376 |
page.goto(BASE_URL, timeout=90000)
|
| 377 |
# Wait for either the logged-in app (tabs) or the login form
|
| 378 |
-
|
| 379 |
-
'button[role=tab], input[type=password]',
|
| 380 |
-
timeout=60000,
|
| 381 |
-
)
|
| 382 |
break
|
| 383 |
except Exception as nav_err:
|
| 384 |
remaining = int(deadline - time.time())
|
|
@@ -391,11 +440,12 @@ def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
|
|
| 391 |
|
| 392 |
# If we landed on the login page (session expired or space rebuilt), re-login
|
| 393 |
try:
|
| 394 |
-
if page.locator('
|
| 395 |
print(" 🔑 Session expired — re-logging in...")
|
| 396 |
_do_login(page)
|
| 397 |
except Exception:
|
| 398 |
pass # Already on the app — no login needed
|
|
|
|
| 399 |
|
| 400 |
|
| 401 |
def submit_job(page: Page, test_case: dict):
|
|
@@ -1653,6 +1703,30 @@ def save_summary_md(run: dict, json_path: Path, env_name: str = "") -> Path:
|
|
| 1653 |
return md_path
|
| 1654 |
|
| 1655 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1656 |
# ---------------------------------------------------------------------------
|
| 1657 |
# Main
|
| 1658 |
# ---------------------------------------------------------------------------
|
|
@@ -1664,9 +1738,11 @@ def run_quality_suite(max_tests: int = None, env_name: str = "", suite_override:
|
|
| 1664 |
suite = suite_override or build_test_suite(config)
|
| 1665 |
if max_tests:
|
| 1666 |
suite = suite[:max_tests]
|
|
|
|
| 1667 |
|
| 1668 |
print(f"\n{'='*62}")
|
| 1669 |
print(f" DemoPrep Quality Run — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
|
|
|
| 1670 |
print(f" Target: {BASE_URL}")
|
| 1671 |
print(f" {len(suite)} tests | "
|
| 1672 |
f"{sum(1 for t in suite if t['type']=='fixed')} fixed "
|
|
@@ -1679,7 +1755,6 @@ def run_quality_suite(max_tests: int = None, env_name: str = "", suite_override:
|
|
| 1679 |
label = {"fixed": "🔒", "random": "🎲", "ai_generated": "🤖", "custom": "✏️"}[tc["type"]]
|
| 1680 |
print(f" [{i}] {label} {tc['name']}")
|
| 1681 |
|
| 1682 |
-
run_id = str(uuid.uuid4())[:8]
|
| 1683 |
results = []
|
| 1684 |
|
| 1685 |
with sync_playwright() as p:
|
|
@@ -1744,7 +1819,8 @@ def run_quality_suite(max_tests: int = None, env_name: str = "", suite_override:
|
|
| 1744 |
"test_count": len(results), "tests": results,
|
| 1745 |
}
|
| 1746 |
path = save_results(run)
|
| 1747 |
-
save_summary_md(run, path, env_name=env_name)
|
|
|
|
| 1748 |
|
| 1749 |
# --- Summary table ---
|
| 1750 |
try:
|
|
|
|
| 44 |
BASE_URL = os.getenv("TEST_TARGET_URL", "") # may be overridden by --url flag at runtime
|
| 45 |
TEST_USER = os.getenv("TEST_USER")
|
| 46 |
TEST_PASSWORD = os.getenv("TEST_PASSWORD")
|
| 47 |
+
TEST_NEW_PASSWORD = os.getenv("TEST_NEW_PASSWORD", "")
|
| 48 |
|
| 49 |
CONFIG_FILE = Path(__file__).parent / "quality_config.yaml"
|
| 50 |
RESULTS_DIR = Path(__file__).parent / "quality_results"
|
|
|
|
| 359 |
page.fill('input[type=text]', TEST_USER)
|
| 360 |
page.fill('input[type=password]', TEST_PASSWORD)
|
| 361 |
page.click('button:has-text("Login")')
|
| 362 |
+
_wait_for_visible_app_or_auth_control(page, include_login=False, timeout=90000)
|
| 363 |
+
_handle_forced_password_change(page)
|
| 364 |
page.wait_for_selector('button[role=tab]', timeout=90000)
|
| 365 |
page.wait_for_timeout(3000)
|
| 366 |
|
| 367 |
|
| 368 |
+
def _wait_for_visible_app_or_auth_control(page: Page, *, include_login: bool, timeout: int):
|
| 369 |
+
"""Wait until a visible app tab or auth control is present."""
|
| 370 |
+
page.wait_for_function(
|
| 371 |
+
"""
|
| 372 |
+
({ includeLogin }) => {
|
| 373 |
+
const visible = (el) => !!(
|
| 374 |
+
el &&
|
| 375 |
+
(el.offsetWidth || el.offsetHeight || el.getClientRects().length)
|
| 376 |
+
);
|
| 377 |
+
const hasVisibleTab = Array.from(document.querySelectorAll('button[role="tab"]'))
|
| 378 |
+
.some(visible);
|
| 379 |
+
if (hasVisibleTab) return true;
|
| 380 |
+
|
| 381 |
+
const buttons = Array.from(document.querySelectorAll('button'))
|
| 382 |
+
.filter(visible)
|
| 383 |
+
.map((button) => (button.textContent || '').trim());
|
| 384 |
+
if (buttons.some((text) => text.includes('Change Password'))) return true;
|
| 385 |
+
if (includeLogin && buttons.some((text) => text.includes('Login'))) return true;
|
| 386 |
+
return false;
|
| 387 |
+
}
|
| 388 |
+
""",
|
| 389 |
+
arg={"includeLogin": include_login},
|
| 390 |
+
timeout=timeout,
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def _handle_forced_password_change(page: Page):
|
| 395 |
+
"""Handle or explicitly fail on the app's temporary-password gate."""
|
| 396 |
+
try:
|
| 397 |
+
gate = page.get_by_text("Change Password Required", exact=False)
|
| 398 |
+
if not gate.is_visible(timeout=1500):
|
| 399 |
+
return
|
| 400 |
+
except Exception:
|
| 401 |
+
return
|
| 402 |
+
|
| 403 |
+
if not TEST_NEW_PASSWORD:
|
| 404 |
+
raise RuntimeError(
|
| 405 |
+
"Test user is blocked by the temporary-password gate. "
|
| 406 |
+
"Clear must_change_password for TEST_USER or set TEST_NEW_PASSWORD "
|
| 407 |
+
"so the harness can complete the required password change."
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
page.locator('input[placeholder="Enter the password you just used to sign in"]').first.fill(TEST_PASSWORD)
|
| 411 |
+
page.locator('input[placeholder="At least 8 characters"]').first.fill(TEST_NEW_PASSWORD)
|
| 412 |
+
page.locator('input[placeholder="Repeat new password"]').first.fill(TEST_NEW_PASSWORD)
|
| 413 |
+
page.click('button:has-text("Change Password")', timeout=5000)
|
| 414 |
+
page.wait_for_selector('button[role=tab]', timeout=30000)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
|
| 418 |
"""
|
| 419 |
Navigate to BASE_URL and ensure we're on the logged-in app.
|
|
|
|
| 427 |
try:
|
| 428 |
page.goto(BASE_URL, timeout=90000)
|
| 429 |
# Wait for either the logged-in app (tabs) or the login form
|
| 430 |
+
_wait_for_visible_app_or_auth_control(page, include_login=True, timeout=60000)
|
|
|
|
|
|
|
|
|
|
| 431 |
break
|
| 432 |
except Exception as nav_err:
|
| 433 |
remaining = int(deadline - time.time())
|
|
|
|
| 440 |
|
| 441 |
# If we landed on the login page (session expired or space rebuilt), re-login
|
| 442 |
try:
|
| 443 |
+
if page.locator('button:has-text("Login")').is_visible(timeout=2000):
|
| 444 |
print(" 🔑 Session expired — re-logging in...")
|
| 445 |
_do_login(page)
|
| 446 |
except Exception:
|
| 447 |
pass # Already on the app — no login needed
|
| 448 |
+
_handle_forced_password_change(page)
|
| 449 |
|
| 450 |
|
| 451 |
def submit_job(page: Page, test_case: dict):
|
|
|
|
| 1703 |
return md_path
|
| 1704 |
|
| 1705 |
|
| 1706 |
+
def print_handoff_block(run: dict, json_path: Path, md_path: Path, env_name: str = ""):
|
| 1707 |
+
latest_name = f"latest_{env_name}_summary.md" if env_name else "latest_summary.md"
|
| 1708 |
+
latest_path = RESULTS_DIR / latest_name
|
| 1709 |
+
timestamp = run.get("timestamp", "")
|
| 1710 |
+
run_id = run.get("run_id", "")
|
| 1711 |
+
target = run.get("target_url", "")
|
| 1712 |
+
avg = run.get("avg_score", "")
|
| 1713 |
+
grade = run.get("overall_grade", "")
|
| 1714 |
+
|
| 1715 |
+
print("\n📌 Agent handoff")
|
| 1716 |
+
print(f" Run ID: {run_id}")
|
| 1717 |
+
print(f" Timestamp: {timestamp}")
|
| 1718 |
+
print(f" Target: {target}")
|
| 1719 |
+
print(f" Results: {json_path}")
|
| 1720 |
+
print(f" Summary: {md_path}")
|
| 1721 |
+
print(f" Latest: {latest_path}")
|
| 1722 |
+
print(
|
| 1723 |
+
" Paste this: "
|
| 1724 |
+
f"DemoPrep quality run {run_id} ({timestamp}) "
|
| 1725 |
+
f"avg={avg}/{grade} target={target} "
|
| 1726 |
+
f"results={json_path} summary={md_path}"
|
| 1727 |
+
)
|
| 1728 |
+
|
| 1729 |
+
|
| 1730 |
# ---------------------------------------------------------------------------
|
| 1731 |
# Main
|
| 1732 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1738 |
suite = suite_override or build_test_suite(config)
|
| 1739 |
if max_tests:
|
| 1740 |
suite = suite[:max_tests]
|
| 1741 |
+
run_id = str(uuid.uuid4())[:8]
|
| 1742 |
|
| 1743 |
print(f"\n{'='*62}")
|
| 1744 |
print(f" DemoPrep Quality Run — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
| 1745 |
+
print(f" Run ID: {run_id}")
|
| 1746 |
print(f" Target: {BASE_URL}")
|
| 1747 |
print(f" {len(suite)} tests | "
|
| 1748 |
f"{sum(1 for t in suite if t['type']=='fixed')} fixed "
|
|
|
|
| 1755 |
label = {"fixed": "🔒", "random": "🎲", "ai_generated": "🤖", "custom": "✏️"}[tc["type"]]
|
| 1756 |
print(f" [{i}] {label} {tc['name']}")
|
| 1757 |
|
|
|
|
| 1758 |
results = []
|
| 1759 |
|
| 1760 |
with sync_playwright() as p:
|
|
|
|
| 1819 |
"test_count": len(results), "tests": results,
|
| 1820 |
}
|
| 1821 |
path = save_results(run)
|
| 1822 |
+
md_path = save_summary_md(run, path, env_name=env_name)
|
| 1823 |
+
print_handoff_block(run, path, md_path, env_name=env_name)
|
| 1824 |
|
| 1825 |
# --- Summary table ---
|
| 1826 |
try:
|
tests/test_dataset_first_builders.py
CHANGED
|
@@ -259,6 +259,51 @@ def test_grocery_finance_routes_to_cpg_financials_not_sales_fact():
|
|
| 259 |
assert "LTV_USD" not in fact_columns
|
| 260 |
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
def test_function_specific_routing_overrides_industry_terms():
|
| 263 |
cases = [
|
| 264 |
("Financial Services", "Banking", "HR", "workforce_hr"),
|
|
@@ -306,6 +351,71 @@ def test_it_operations_has_two_year_hourly_calendar():
|
|
| 306 |
assert max(row["INCIDENTS"] for row in fact.rows[:100]) <= 10
|
| 307 |
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
def test_new_quality_pool_domains_are_not_generic_finance_or_pipeline():
|
| 310 |
cases = [
|
| 311 |
("J.B. Hunt", "jbhunt.com", "Transportation & Logistics", "Trucking Finance", "Finance", "trucking_finance", "TRUCKING_FINANCIALS", {"LOADS", "REVENUE_PER_LOADED_MILE_USD", "EMPTY_MILE_PCT"}),
|
|
|
|
| 259 |
assert "LTV_USD" not in fact_columns
|
| 260 |
|
| 261 |
|
| 262 |
+
def test_automotive_supplier_sales_routes_to_supplier_programs():
|
| 263 |
+
build = build_dataset_first_demo(
|
| 264 |
+
company_name="BorgWarner",
|
| 265 |
+
company_url="https://borgwarner.com",
|
| 266 |
+
use_case="Automotive Sales",
|
| 267 |
+
vertical="Manufacturing",
|
| 268 |
+
function="Sales",
|
| 269 |
+
row_count_guidance=100,
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
assert build is not None
|
| 273 |
+
assert build.scenario.scenario_type == "automotive_supplier_sales"
|
| 274 |
+
table_map = build.dataset.table_map()
|
| 275 |
+
assert "SUPPLIER_PROGRAM_SALES" in table_map
|
| 276 |
+
assert "VEHICLE_SALES" not in table_map
|
| 277 |
+
fact_columns = {column.name for column in table_map["SUPPLIER_PROGRAM_SALES"].columns}
|
| 278 |
+
assert {
|
| 279 |
+
"RFQS_RECEIVED",
|
| 280 |
+
"QUOTED_REVENUE_USD",
|
| 281 |
+
"AWARDED_REVENUE_USD",
|
| 282 |
+
"BACKLOG_USD",
|
| 283 |
+
"TOOLING_RECOVERY_USD",
|
| 284 |
+
"WARRANTY_RESERVE_USD",
|
| 285 |
+
"PROGRAM_MARGIN_USD",
|
| 286 |
+
"DEFECT_PPM",
|
| 287 |
+
} <= fact_columns
|
| 288 |
+
assert "DEALER_HOLDBACK_USD" not in fact_columns
|
| 289 |
+
assert "TEST_DRIVES" not in fact_columns
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def test_vehicle_oem_sales_stays_on_automotive_sales():
|
| 293 |
+
build = build_dataset_first_demo(
|
| 294 |
+
company_name="Stellantis",
|
| 295 |
+
company_url="https://stellantis.com",
|
| 296 |
+
use_case="Automotive Sales",
|
| 297 |
+
vertical="Manufacturing",
|
| 298 |
+
function="Sales",
|
| 299 |
+
row_count_guidance=100,
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
assert build is not None
|
| 303 |
+
assert build.scenario.scenario_type == "automotive_sales"
|
| 304 |
+
assert "VEHICLE_SALES" in build.dataset.table_map()
|
| 305 |
+
|
| 306 |
+
|
| 307 |
def test_function_specific_routing_overrides_industry_terms():
|
| 308 |
cases = [
|
| 309 |
("Financial Services", "Banking", "HR", "workforce_hr"),
|
|
|
|
| 351 |
assert max(row["INCIDENTS"] for row in fact.rows[:100]) <= 10
|
| 352 |
|
| 353 |
|
| 354 |
+
def test_medical_device_inventory_dimensions_are_not_numbered_fillers():
|
| 355 |
+
build = build_dataset_first_demo(
|
| 356 |
+
company_name="Intuitive Surgical",
|
| 357 |
+
company_url="https://www.intuitive.com",
|
| 358 |
+
use_case="Medical device manufacturing operations inventory and warehouse supply chain",
|
| 359 |
+
vertical="Manufacturing",
|
| 360 |
+
function="Operations",
|
| 361 |
+
row_count_guidance=100,
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
assert build is not None
|
| 365 |
+
assert build.scenario.scenario_type == "inventory_supply_chain"
|
| 366 |
+
|
| 367 |
+
table_map = build.dataset.table_map()
|
| 368 |
+
products = [row["PRODUCT_NAME"] for row in table_map["PRODUCTS"].rows]
|
| 369 |
+
warehouses = [row["WAREHOUSE_NAME"] for row in table_map["WAREHOUSES"].rows]
|
| 370 |
+
suppliers = [row["SUPPLIER_NAME"] for row in table_map["SUPPLIERS"].rows]
|
| 371 |
+
regions = [row["REGION_NAME"] for row in table_map["REGIONS"].rows]
|
| 372 |
+
|
| 373 |
+
assert len(products) == 36
|
| 374 |
+
assert len(warehouses) == 24
|
| 375 |
+
assert len(suppliers) == 24
|
| 376 |
+
assert "da Vinci 5 Surgical System" in products
|
| 377 |
+
assert "Sunnyvale Final Assembly" in warehouses
|
| 378 |
+
assert "Precision Motion Components" in suppliers
|
| 379 |
+
assert "Manufacturing Sites" in regions
|
| 380 |
+
|
| 381 |
+
generated_names = products + warehouses + suppliers
|
| 382 |
+
assert not any("Product 29" in name or "Warehouse 21" in name or "Supplier 19" in name for name in generated_names)
|
| 383 |
+
assert not any(name.rsplit(" ", 1)[-1].isdigit() for name in generated_names)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def test_higher_education_enrollment_dimensions_are_not_numbered_fillers():
|
| 387 |
+
build = build_dataset_first_demo(
|
| 388 |
+
company_name="Wake Forest University",
|
| 389 |
+
company_url="https://www.wfu.edu",
|
| 390 |
+
use_case="Student Success Enrollment",
|
| 391 |
+
vertical="Education",
|
| 392 |
+
function="Operations",
|
| 393 |
+
row_count_guidance=100,
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
assert build is not None
|
| 397 |
+
assert build.scenario.scenario_type == "education_enrollment"
|
| 398 |
+
|
| 399 |
+
table_map = build.dataset.table_map()
|
| 400 |
+
programs = [row["PROGRAM_NAME"] for row in table_map["PROGRAMS"].rows]
|
| 401 |
+
campuses = [row["CAMPUS_NAME"] for row in table_map["CAMPUSES"].rows]
|
| 402 |
+
segments = [row["STUDENT_SEGMENT_NAME"] for row in table_map["STUDENT_SEGMENTS"].rows]
|
| 403 |
+
terms = [row["TERM_NAME"] for row in table_map["TERMS"].rows]
|
| 404 |
+
|
| 405 |
+
assert len(programs) == 18
|
| 406 |
+
assert len(campuses) == 18
|
| 407 |
+
assert len(segments) == 18
|
| 408 |
+
assert len(terms) == 8
|
| 409 |
+
assert "School of Business" in programs
|
| 410 |
+
assert "Reynolda Campus" in campuses
|
| 411 |
+
assert "First-Generation Students" in segments
|
| 412 |
+
assert "Fall Early Decision I" in terms
|
| 413 |
+
|
| 414 |
+
generated_names = programs + campuses + segments + terms
|
| 415 |
+
assert not any("Program 17" in name or "Campus 18" in name or "Student Segment 14" in name or "Term 07" in name for name in generated_names)
|
| 416 |
+
assert not any(name.rsplit(" ", 1)[-1].isdigit() for name in generated_names)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
def test_new_quality_pool_domains_are_not_generic_finance_or_pipeline():
|
| 420 |
cases = [
|
| 421 |
("J.B. Hunt", "jbhunt.com", "Transportation & Logistics", "Trucking Finance", "Finance", "trucking_finance", "TRUCKING_FINANCIALS", {"LOADS", "REVENUE_PER_LOADED_MILE_USD", "EMPTY_MILE_PCT"}),
|