diff --git a/agents/venture_discovery.py b/agents/venture_discovery.py index 96da7b9..4058c7c 100644 --- a/agents/venture_discovery.py +++ b/agents/venture_discovery.py @@ -16,7 +16,7 @@ from django.db import close_old_connections, connection from django.utils import timezone from control_plane.events.bus import EventBus -from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CohortIdeationMandate, CompanyBoardReview, CompanyCapabilityRequirement, CompanyMandate, CompanyProposal, CompanyProposalStatus, EvidenceTier, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, NoveltyGateDecision, OpportunityTerritory, OverlapClassification, PortfolioCapabilityGap, PortfolioICReview, PortfolioSaturationAnalysis, PortfolioThesis, PortfolioThesisCluster, PortfolioThesisStatus, ThesisMatchClassification, VentureArtifact, VentureCapabilityDemand, VentureCohort, VentureCohortMember, VentureCollision, VentureGenerationRejection, VentureThesis, VentureThesisFingerprint +from control_plane.ventures.models import AutonomousCandidateStatus, AutonomousGateResult, AutonomousOperabilityAssessment, CapabilityPriority, CapabilityStatus, CohortIdeationMandate, CompanyBoardReview, CompanyCapabilityRequirement, CompanyMandate, CompanyProposal, CompanyProposalStatus, EvidenceTier, FounderDependencyLevel, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, NoveltyGateDecision, OpportunityTerritory, OverlapClassification, PortfolioCapabilityGap, PortfolioICReview, PortfolioSaturationAnalysis, PortfolioThesis, PortfolioThesisCluster, PortfolioThesisStatus, ThesisMatchClassification, VentureArtifact, VentureCapabilityDemand, VentureCohort, VentureCohortMember, VentureCollision, VentureGenerationRejection, VentureThesis, VentureThesisFingerprint, VentureTrack from graph.models import GraphRun, GraphRunStatus from model_router.providers import extract_json_object from model_router.policy import model_for_role @@ -25,7 +25,7 @@ from research.searxng import SearxngSearchClient, WebPageFetcher PITCH_SECTIONS = ["Company name", "One-line thesis", "Problem", "ICP", "Why now", "Product / service", "Business model", "Pricing", "Route to first customer", "Validation plan", "$50 capital allocation proposal", "Time to first dollar", "Path to $500 net cash", "Competition", "Differentiation", "Build requirements", "Distribution requirements", "Risks", "What would falsify the thesis", "Confidence"] -SCORE_DIMENSIONS = ["Demand Evidence", "Time-to-First-Dollar Attractiveness", "Capital Efficiency", "Validation Affordability", "Gross Margin Potential", "Distribution Feasibility", "Build Simplicity", "Defensibility", "Market Opportunity", "Competitive Position", "Risk Manageability", "AI Leverage", "Platformization Potential", "Probability of Reaching $500"] +SCORE_DIMENSIONS = ["Demand Evidence", "Time-to-First-Dollar Attractiveness", "Capital Efficiency", "Validation Affordability", "Gross Margin Potential", "Distribution Feasibility", "Build Simplicity", "Defensibility", "Market Opportunity", "Competitive Position", "Risk Manageability", "AI Leverage", "Platformization Potential", "Autonomous Operability", "Probability of Reaching $500"] SCORE_DEFINITIONS = {dimension: "100 = highly attractive; 0 = highly unattractive" for dimension in SCORE_DIMENSIONS} EVIDENCE_CEILINGS = {EvidenceTier.TIER_0_THESIS: 35, EvidenceTier.TIER_1_PUBLIC_EVIDENCE: 55, EvidenceTier.TIER_2_CUSTOMER_SIGNAL: 70, EvidenceTier.TIER_3_WILLINGNESS_TO_PAY: 85, EvidenceTier.TIER_4_PAID_CUSTOMER: 95, EvidenceTier.TIER_5_REPEATABLE_TRACTION: 100} AI_NATIVE_POLICY = {"preference": "Prefer AI-native opportunities where Artifex can build or deliver most value with existing agents, local inference, and owned workflow capabilities.", "preferred_classes": ["AI wrapper platforms", "AI-enabled services", "AI infrastructure / developer tools", "AI research / intelligence products", "AI automation for SMB / enterprise workflows"], "soft_portfolio_targets": {"ai_wrapper_platforms": "40-50%", "ai_enabled_services": "20-30%", "developer_infrastructure_intelligence": "10-20%", "non_ai_economic_outliers": "10-20%"}, "penalize_concentration": ["Shopify/ecommerce", "generic audits", "emergency fix services", "one-off consulting", "identical outbound-led service models"], "do_not": "Do not let AI novelty outweigh customer demand."} @@ -34,6 +34,7 @@ DEFAULT_SOFT_EXCLUSIONS = ["generic contract scanners", "Shopify audit products" DEFAULT_TERRITORY_ALLOCATION = [OpportunityTerritory.VERTICAL_AI_WRAPPERS, OpportunityTerritory.VERTICAL_AI_WRAPPERS, OpportunityTerritory.ENTERPRISE_WORKFLOW_AUTOMATION, OpportunityTerritory.SMB_AUTOMATION, OpportunityTerritory.DEVELOPER_AI_INFRASTRUCTURE, OpportunityTerritory.INTELLIGENCE_MONITORING, OpportunityTerritory.DATA_DOCUMENT_AUTOMATION, OpportunityTerritory.AI_ENABLED_SERVICE_TO_PLATFORM, OpportunityTerritory.OPEN_CATEGORY, OpportunityTerritory.OPEN_CATEGORY] DEFAULT_DUPLICATE_POLICY = {"max_near_duplicate_per_thesis_per_cohort": 1, "max_competitive_per_thesis_per_cohort": 2, "max_attempts_per_slot": 3, "cohort_attempt_budget_multiplier": 4} RESEARCH_CATEGORIES = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"] +AUTONOMOUS_ARCHETYPES = ["SELF_SERVICE_AI_TOOL", "AUTOMATED_MONITORING_PRODUCT", "DIGITAL_ANALYSIS_SERVICE", "AUTOMATED_TRANSFORMATION_SERVICE", "MICRO_SAAS", "DATA_INTELLIGENCE_SUBSCRIPTION", "DEVELOPER_TOOL", "AUTONOMOUS_DIGITAL_PRODUCT", "MARKETPLACE_DELIVERED_SERVICE", "AGENT_AS_A_SERVICE"] class VentureDiscoveryService: @@ -49,10 +50,10 @@ class VentureDiscoveryService: def create_v0_mandate(self) -> CompanyMandate: mandate = CompanyMandate.objects.create( - objective="Design a business that could plausibly turn at most $50 of external validation capital into $500 net new cash.", - constraints={"external_validation_capital_max": 50, "target_net_new_cash": 500, "target_window_days": 30, "no_equity_raise": True, "no_debt": True, "no_illegal_or_deceptive_activity": True, "no_spam": True, "no_fake_traction": True, "no_fabricated_customer_evidence": True, "no_real_spend_in_v0": True, "no_real_customer_outreach_in_v0": True, "existing_artifex_compute_sunk_available": True}, - optimization_targets=["time to first dollar", "capital efficiency", "real demand evidence", "high gross margin", "realistic execution", "low external dependency", "ability to validate cheaply", "AI leverage where economically justified", "service-to-platform evolution potential"], - metadata={"milestone": "VENTURE_DISCOVERY_V0", "spend_authorized": False, "customer_outreach_authorized": False, "ai_native_policy": AI_NATIVE_POLICY}, + objective="Design a business that Artifex itself could plausibly operate and turn at most $50 of external validation capital into at least $500 net new cash within 30 days, with no more than 30 minutes/week of routine human intervention.", + constraints={"external_validation_capital_max": 50, "target_net_new_cash": 500, "target_window_days": 30, "max_human_routine_minutes_per_week": 30, "default_venture_track": VentureTrack.AUTONOMOUS, "routine_human_involvement_not_allowed": ["founder-led sales calls", "manual prospecting", "routine support", "manual fulfillment", "bespoke consulting", "routine QA", "manual payment chasing", "routine onboarding"], "allowed_human_involvement": ["approve account/legal setup", "approve initial spend", "approve material legal commitments", "exceptional safety/security escalation", "irreversible capital decisions"], "no_equity_raise": True, "no_debt": True, "no_illegal_or_deceptive_activity": True, "no_spam": True, "no_fake_traction": True, "no_fabricated_customer_evidence": True, "no_real_spend_in_v0": True, "no_real_customer_outreach_in_v0": True, "existing_artifex_compute_sunk_available": True, "validation_deployment": "SUBDOMAIN_DEPLOYMENT"}, + optimization_targets=["autonomous fulfillment", "autonomous customer acquisition", "self-service onboarding", "standardized digital delivery", "AI leverage", "platformization", "recurring revenue", "simple payment flow", "low support burden", "low regulatory burden", "low integration burden", "rapid validation"], + metadata={"milestone": "VENTURE_DISCOVERY_V0_AUTONOMOUS", "venture_track": VentureTrack.AUTONOMOUS, "spend_authorized": False, "customer_outreach_authorized": False, "ai_native_policy": AI_NATIVE_POLICY, "autonomous_archetypes": AUTONOMOUS_ARCHETYPES}, ) self._artifact(None, mandate, "VENTURE_MANDATE", "Venture Discovery V0 Mandate", {"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets}, self._readable_mandate(mandate), "venture_discovery") return mandate @@ -92,7 +93,7 @@ class VentureDiscoveryService: confidence=confidence, status=CompanyProposalStatus.SUBMITTED, pitch=pitch, - metadata={"generation_source": source, "fallback_evidence": source == "deterministic_fallback", "web_research_available": self.web_research_available, "real_spend": 0, "real_customer_outreach": False}, + metadata={"generation_source": source, "fallback_evidence": source == "deterministic_fallback", "web_research_available": self.web_research_available, "real_spend": 0, "real_customer_outreach": False, "validation_offer": payload.get("validation_offer", {}), "end_state_business_model": payload.get("end_state_business_model", {}), "fulfillment_contract": payload.get("fulfillment_contract", {}), "minutes_per_week_human": payload.get("minutes_per_week_human", None), "human_actions_required": self._as_list(payload.get("human_actions_required", []))}, evidence_tier=EvidenceTier.TIER_0_THESIS, ) self._artifact(proposal, mandate, "STANDARDIZED_COMPANY_PITCH", "Standardized Company Pitch", pitch, self.readable_pitch(pitch), f"Company Brain/{source}" if source != "deterministic_fallback" else "deterministic_fallback", graph_run=graph_run) @@ -104,6 +105,10 @@ class VentureDiscoveryService: research = {"coverage": {category: False for category in required}, "sources": [], "findings": {}, "unverified_categories": required, "research_available": False} search_sources = self._searxng_sources(proposal, required, depth=depth) if self.web_research_available else [] page_corpus = self._page_corpus(search_sources) if search_sources else [] + diagnostics = {"search_result_count": len(search_sources), "page_fetch_count": len(page_corpus), "model_provider": self.research_model_hint if self.router is not None else "none", "search_provider": "searxng" if search_sources else "none"} + search_diagnostics = getattr(self, "_last_search_diagnostics", {}) + if search_diagnostics: + diagnostics["search_diagnostics"] = search_diagnostics if self.web_research_available and self.router is not None: try: response = self.router.complete(ModelRequestContract(purpose=ModelCapability.REASONING, model_hint=self.research_model_hint, prompt="Bounded public web market research for exactly one startup pitch. Return JSON with keys: sources (list of {url,title,category,summary}), findings (object keyed by competitors, pricing, customer_pain, market_alternatives, regulatory_platform_risks), and coverage (object with each required key true/false). Use only the provided SearXNG search context and fetched page excerpts for source URLs; do not fabricate URLs. If a category has no strong relevant source, mark coverage false. Research depth: " + depth + ". Search context: " + json.dumps(search_sources, default=str) + " Page excerpts: " + json.dumps(page_corpus, default=str) + " Pitch: " + json.dumps(proposal.pitch, default=str))) @@ -119,7 +124,7 @@ class VentureDiscoveryService: if page_corpus: research = {**research, "page_corpus": page_corpus, "page_fetch_count": len(page_corpus)} if not research.get("sources"): - research = self._missing_research(required, research.get("failure", "public web research unavailable or returned no source-linked evidence")) + research = {**self._missing_research(required, research.get("failure", "public web research unavailable or returned no source-linked evidence")), **diagnostics} quality = self.filter_research_sources(proposal, research.get("sources", []), required) research["sources"] = quality["accepted_sources"] research["source_rejections"] = quality["rejected_sources"] @@ -134,7 +139,8 @@ class VentureDiscoveryService: proposal.confidence = round(min(float(proposal.confidence), 0.45 + 0.35 * coverage_ratio), 2) existing_research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {} proposal.market_evidence = [*self._as_list(proposal.market_evidence), *research.get("sources", []), {"type": "research_coverage", "source": "venture_research", "summary": f"{depth.title()} source-linked research coverage: {round(coverage_ratio * 100)}%", "coverage": coverage, "unverified_categories": research["unverified_categories"], "depth": depth}] - proposal.metadata = {**proposal.metadata, "research": {**existing_research, "coverage_ratio": coverage_ratio, "coverage": coverage, "unverified_categories": research["unverified_categories"], "source_count": len(research.get("sources", [])), "source_rejection_count": len(quality["rejected_sources"]), "page_fetch_count": research.get("page_fetch_count", 0), "provider": research.get("provider", "none"), "search_provider": research.get("search_provider", "none"), "depth": depth, "explicit_research_failure": research.get("failure", "") if coverage_ratio < 0.8 else "", "before_deep_coverage_ratio": existing_research.get("coverage_ratio") if depth == "deep" else existing_research.get("before_deep_coverage_ratio")}} + explicit_failure = "" if research.get("sources") else research.get("failure", "public web research unavailable or returned no source-linked evidence") + proposal.metadata = {**proposal.metadata, "research": {**existing_research, "coverage_ratio": coverage_ratio, "coverage": coverage, "unverified_categories": research["unverified_categories"], "source_count": len(research.get("sources", [])), "source_rejection_count": len(quality["rejected_sources"]), "search_result_count": research.get("search_result_count", len(search_sources)), "page_fetch_count": research.get("page_fetch_count", len(page_corpus)), "provider": research.get("provider", diagnostics.get("model_provider", "none")), "search_provider": research.get("search_provider", diagnostics["search_provider"]), "search_diagnostics": research.get("search_diagnostics", diagnostics.get("search_diagnostics", {})), "model_research_failure": research.get("failure", "") if research.get("sources") else "", "depth": depth, "explicit_research_failure": explicit_failure, "before_deep_coverage_ratio": existing_research.get("coverage_ratio") if depth == "deep" else existing_research.get("before_deep_coverage_ratio")}} proposal.evidence_tier = EvidenceTier.TIER_1_PUBLIC_EVIDENCE if coverage_ratio > 0 else EvidenceTier.TIER_0_THESIS if proposal.thesis: proposal.thesis.evidence_tier = proposal.evidence_tier @@ -208,7 +214,13 @@ class VentureDiscoveryService: return response def score_and_decide(self, diligence: ICDiligence, *, graph_run=None) -> ICDecision: + try: + assessment = diligence.proposal.autonomous_assessment + except ObjectDoesNotExist: + assessment = None scores = self._evidence_scores(diligence.proposal) + if assessment is not None: + scores["Autonomous Operability"] = int(assessment.autonomous_operability_score) composite = round(sum(scores.values()) / len(scores), 1) calibration = self.calibrate_probability(diligence.proposal, raw_probability=float(scores["Probability of Reaching $500"])) scores["Probability of Reaching $500"] = int(calibration["evidence_adjusted_probability"]) @@ -419,6 +431,7 @@ class VentureDiscoveryService: self.red_team(diligence) self.final_company_response(diligence) self.score_and_decide(diligence) + self.assess_autonomous_operability(proposal) gap = self.capability_analysis(proposal) self.produce_investment_memo(diligence, gap) member.child_graph_run = GraphRun.objects.create(execution_graph_version=cohort.graph_run.execution_graph_version if cohort.graph_run else None, status=GraphRunStatus.COMPLETE, metadata={"logical_child_company_run": True, "proposal_id": str(proposal.id), "cohort_id": cohort.cohort_id}) if cohort.graph_run else None @@ -431,7 +444,85 @@ class VentureDiscoveryService: cohort.status = "INDIVIDUAL_DILIGENCE_COMPLETE" cohort.save(update_fields=["metrics", "status", "updated_at"]) + def assess_autonomous_operability_for_cohort(self, cohort: VentureCohort) -> list[AutonomousOperabilityAssessment]: + assessments = [self.assess_autonomous_operability(member.proposal) for member in cohort.members.select_related("proposal")] + cohort.metrics = {**cohort.metrics, "autonomous_eligible_count": sum(1 for item in assessments if item.gate_result == AutonomousGateResult.AUTONOMOUS_ELIGIBLE), "assisted_only_count": sum(1 for item in assessments if item.gate_result == AutonomousGateResult.ASSISTED_ONLY)} + cohort.save(update_fields=["metrics", "updated_at"]) + return assessments + + def assess_autonomous_operability(self, proposal: CompanyProposal) -> AutonomousOperabilityAssessment: + text = " ".join([proposal.title, proposal.description, proposal.problem, proposal.target_customer, proposal.proposed_solution, proposal.business_model, proposal.pricing_hypothesis, proposal.acquisition_strategy, proposal.validation_plan, proposal.differentiation]).lower() + positive_terms = ["self-service", "automated", "monitor", "subscription", "api", "dashboard", "report", "digital", "crawl", "alert", "standardized", "recurring", "checkout", "plugin", "cli", "no-code"] + negative_causes = { + "SALES": ["sales call", "enterprise sales", "procurement", "relationship"], + "DOMAIN_EXPERTISE": ["expert", "clinical", "medical", "licensed", "engineer review"], + "CUSTOMER_TRUST": ["trust", "advisor", "consultant"], + "RELATIONSHIP_MANAGEMENT": ["account management", "relationship"], + "MANUAL_QA": ["manual qa", "manual review", "human review"], + "REGULATORY_SIGNOFF": ["regulatory sign-off", "compliance signoff", "approval"], + "CUSTOM_IMPLEMENTATION": ["custom implementation", "integration", "bespoke"], + "SUPPORT_ESCALATION": ["high-touch support", "white glove"], + "OFFLINE_ACTIVITY": ["onsite", "offline", "in person"], + "LEGAL_JUDGMENT": ["legal judgment", "legal advice", "lawyer"], + "NEGOTIATION": ["negotiation"], + "PERSONAL_BRAND": ["personal brand", "founder credibility"], + } + causes = [cause for cause, terms in negative_causes.items() if any(term in text for term in terms)] + score = 45 + min(30, sum(5 for term in positive_terms if term in text)) - min(45, len(causes) * 8) + ai = self.ai_leverage_score(proposal) + platform = self.platformization_potential(proposal) + score += 10 if ai >= 70 else 0 + score += 10 if platform >= 70 else 0 + score = max(0, min(100, score)) + minutes = 5 if score >= 90 else 15 if score >= 80 else 30 if score >= 70 else 90 if score >= 50 else 240 + dependency = FounderDependencyLevel.NONE if minutes <= 5 and not causes else FounderDependencyLevel.LOW if minutes <= 30 and len(causes) <= 1 else FounderDependencyLevel.MEDIUM if minutes <= 90 else FounderDependencyLevel.HIGH if minutes <= 240 else FounderDependencyLevel.CRITICAL + if score >= 80 and dependency in {FounderDependencyLevel.NONE, FounderDependencyLevel.LOW} and ai >= 70 and platform >= 70: + gate = AutonomousGateResult.AUTONOMOUS_ELIGIBLE + track = VentureTrack.AUTONOMOUS + elif score >= 70 and dependency in {FounderDependencyLevel.LOW, FounderDependencyLevel.MEDIUM}: + gate = AutonomousGateResult.AUTONOMOUS_BORDERLINE + track = VentureTrack.AUTONOMOUS + elif score >= 45: + gate = AutonomousGateResult.ASSISTED_ONLY + track = VentureTrack.ASSISTED + else: + gate = AutonomousGateResult.REJECT_OPERABILITY + track = VentureTrack.ASSISTED + loop = self.autonomous_operating_loop(proposal, gate) + contract = self.autonomous_fulfillment_contract(proposal) + assessment, _ = AutonomousOperabilityAssessment.objects.update_or_create( + proposal=proposal, + defaults={"venture_track": track, "gate_result": gate, "autonomous_operability_score": score, "commercial_score": self.commercial_score(proposal), "founder_dependency": dependency, "dependency_causes": causes, "minutes_per_week_human": minutes, "human_actions_required": contract["escalation"], "human_action_categories": ["LEGAL_SETUP", "SPEND_APPROVAL", "EXCEPTION_ESCALATION"] if minutes <= 30 else causes, "operating_loop": loop, "fulfillment_contract": contract, "validation_offer": self.validation_offer(proposal), "end_state_business_model": self.end_state_business_model(proposal), "structural_blockers": causes, "platform_blockers": self.platform_blockers(proposal), "component_scores": {"customer_acquisition_autonomy": loop["discover"]["score"], "onboarding_autonomy": loop["onboard"]["score"], "fulfillment_autonomy": loop["fulfill"]["score"], "support_autonomy": loop["support"]["score"], "billing_payment_autonomy": loop["checkout"]["score"], "quality_verification": loop["verify"]["score"], "repeatability": 90 if "recurring" in text or "subscription" in text else 65}, "rationale": f"{gate}: score {score}, dependency {dependency}, causes {', '.join(causes) or 'none'}."}, + ) + proposal.metadata = {**proposal.metadata, "venture_track": track, "autonomous_operability_score": score, "founder_dependency": dependency, "autonomous_gate_result": gate, "minutes_per_week_human": minutes, "validation_offer": assessment.validation_offer, "end_state_business_model": assessment.end_state_business_model} + proposal.save(update_fields=["metadata", "updated_at"]) + self._artifact(proposal, proposal.mandate, "AUTONOMOUS_OPERABILITY_REPORT", "Autonomous Operability Report", {"score": score, "gate_result": gate, "founder_dependency": dependency, "dependency_causes": causes, "operating_loop": loop, "fulfillment_contract": contract, "validation_offer": assessment.validation_offer, "end_state_business_model": assessment.end_state_business_model, "platform_blockers": assessment.platform_blockers, "structural_blockers": assessment.structural_blockers}, self._readable_memo({"score": score, "gate_result": gate, "founder_dependency": dependency, "dependency_causes": causes, "operating_loop": loop}), "Autonomous Operability IC") + return assessment + + def commercial_score(self, proposal: CompanyProposal) -> float: + diligence = proposal.ic_diligence.order_by("-created_at").first() + return float(diligence.decision.composite_score) if diligence and hasattr(diligence, "decision") else 0.0 + + def autonomous_operating_loop(self, proposal: CompanyProposal, gate: str) -> dict[str, dict[str, Any]]: + automated = gate in {AutonomousGateResult.AUTONOMOUS_ELIGIBLE, AutonomousGateResult.AUTONOMOUS_BORDERLINE} + state = "AUTONOMOUS" if automated else "HUMAN_ROUTINE_REQUIRED" + score = 90 if automated else 35 + return {stage: {"capability": "AVAILABLE" if stage in {"discover", "checkout", "deliver", "measure"} else "PARTIAL", "operation": state, "score": score} for stage in ["discover", "qualify", "acquire", "checkout", "onboard", "fulfill", "verify", "deliver", "support", "measure", "retain_upsell"]} + + def autonomous_fulfillment_contract(self, proposal: CompanyProposal) -> dict[str, Any]: + return {"customer_input": "Customer supplies a URL, file, repository, document, dataset, or narrow workflow input through a self-service intake.", "artifex_process": "Artifex agents run retrieval, analysis/transformation, structured QA, report generation, and delivery workflows.", "customer_output": proposal.proposed_solution, "quality_verification": "Automated checks validate schema completeness, source links, threshold scores, and known failure conditions before delivery.", "failure_recovery": "If verification fails, rerun once with stricter constraints; unresolved failures create exceptional human escalation.", "billing_event": "Payment is collected at checkout before one-off validation delivery or at subscription activation.", "support_model": "Agent-authored help, status updates, and retry guidance; human handles only exceptional safety/legal/account issues.", "escalation": ["material legal commitment", "irreversible spend", "security/safety exception", "verification repeatedly fails"]} + + def validation_offer(self, proposal: CompanyProposal) -> dict[str, Any]: + return {"offer": "$49-$99 fixed-scope automated validation output", "price": "$49-$99", "proof": "willingness-to-pay for a standardized digital result before full product build", "aligned_with_end_state": True} + + def end_state_business_model(self, proposal: CompanyProposal) -> dict[str, Any]: + return {"model": proposal.business_model, "likely_pricing": proposal.pricing_hypothesis, "recurring_path": "subscription, usage, or repeat purchase if validation demand repeats"} + + def platform_blockers(self, proposal: CompanyProposal) -> list[str]: + return ["STRIPE_IMPLEMENTATION_MISSING", "SUBDOMAIN_DEPLOYMENT", "EMAIL_DELIVERY", "SUPPORT_INBOX"] + def portfolio_ic(self, cohort: VentureCohort) -> PortfolioICReview: + self.assess_autonomous_operability_for_cohort(cohort) preliminary_rows = self.portfolio_ranking_rows(cohort) preliminary_top_5 = preliminary_rows[:5] before_coverage = {} @@ -452,15 +543,17 @@ class VentureDiscoveryService: rows = self.portfolio_ranking_rows(cohort) preliminary_rank_by_id = {row["proposal_id"]: index + 1 for index, row in enumerate(preliminary_rows)} ranking_changes = [{"proposal_id": row["proposal_id"], "company": row["company"], "preliminary_rank": preliminary_rank_by_id.get(row["proposal_id"]), "final_rank": index + 1} for index, row in enumerate(rows) if preliminary_rank_by_id.get(row["proposal_id"]) != index + 1] + top_3 = [row for row in rows if row.get("autonomous_gate_result") == AutonomousGateResult.AUTONOMOUS_ELIGIBLE][:3] + top_3_ids = {row["proposal_id"] for row in top_3} for rank, row in enumerate(rows, start=1): member = cohort.members.get(proposal_id=row["proposal_id"]) member.rank = rank - member.is_top_3 = rank <= 3 + member.is_top_3 = row["proposal_id"] in top_3_ids member.portfolio_score = row["portfolio_score"] member.save(update_fields=["rank", "is_top_3", "portfolio_score", "updated_at"]) row["rank"] = rank concentration = self._portfolio_concentration(cohort) - review, _ = PortfolioICReview.objects.update_or_create(cohort=cohort, defaults={"rankings": rows, "top_3": rows[:3], "concentration": concentration, "metadata": {"no_funding": True, "preliminary_rankings": preliminary_rows, "preliminary_top_5": preliminary_top_5, "finalist_research_coverage_before": before_coverage, "finalist_research_coverage_after": after_coverage, "ranking_changes_after_deep_research": ranking_changes}}) + review, _ = PortfolioICReview.objects.update_or_create(cohort=cohort, defaults={"rankings": rows, "top_3": top_3, "concentration": concentration, "metadata": {"no_funding": True, "preliminary_rankings": preliminary_rows, "preliminary_top_5": preliminary_top_5, "finalist_research_coverage_before": before_coverage, "finalist_research_coverage_after": after_coverage, "ranking_changes_after_deep_research": ranking_changes, "autonomous_top_3_shortfall": max(0, 3 - len(top_3))}}) cohort.metrics = {**cohort.metrics, "finalist_deep_research_count": len(preliminary_top_5), "finalist_deep_research_runtime_seconds": round(time.monotonic() - deep_started, 2), "finalist_research_coverage_before": before_coverage, "finalist_research_coverage_after": after_coverage, "ranking_changes_after_deep_research": ranking_changes} cohort.status = "PORTFOLIO_IC_COMPLETE" cohort.save(update_fields=["metrics", "status", "updated_at"]) @@ -475,11 +568,13 @@ class VentureDiscoveryService: for member in cohort.members.select_related("proposal"): proposal = member.proposal decision = proposal.ic_diligence.order_by("-created_at").first().decision + assessment = self.assess_autonomous_operability(proposal) capability_burden = proposal.capability_requirements.filter(status=CapabilityStatus.MISSING).count() concentration_penalty = self._generic_concentration_penalty(proposal) ai_bonus = (decision.component_scores.get("AI Leverage", 0) * 0.08) + (decision.component_scores.get("Platformization Potential", 0) * 0.06) - score = round(decision.composite_score + decision.probability_500_within_30_days * 0.2 + ai_bonus - capability_burden * 1.5 - collision_risk[str(proposal.id)] * 2 - concentration_penalty, 1) - rows.append({"proposal_id": str(proposal.id), "company": proposal.title, "thesis": proposal.pitch.get("One-line thesis", proposal.description), "ic_score": decision.composite_score, "probability": decision.probability_500_within_30_days, "decision": decision.decision, "evidence_tier": decision.evidence_tier, "initial_tranche": str(decision.initial_tranche or "0"), "ai_leverage": decision.component_scores.get("AI Leverage", 0), "platformization_potential": decision.component_scores.get("Platformization Potential", 0), "portfolio_score": score, "capability_burden": capability_burden, "collision_risk": collision_risk[str(proposal.id)], "concentration_penalty": concentration_penalty}) + autonomy_penalty = 35 if assessment.gate_result == AutonomousGateResult.ASSISTED_ONLY else 70 if assessment.gate_result == AutonomousGateResult.REJECT_OPERABILITY else 12 if assessment.gate_result == AutonomousGateResult.AUTONOMOUS_BORDERLINE else 0 + score = round(decision.composite_score * 0.45 + assessment.autonomous_operability_score * 0.45 + decision.probability_500_within_30_days * 0.12 + ai_bonus - capability_burden * 1.5 - collision_risk[str(proposal.id)] * 2 - concentration_penalty - autonomy_penalty, 1) + rows.append({"proposal_id": str(proposal.id), "company": proposal.title, "thesis": proposal.pitch.get("One-line thesis", proposal.description), "ic_score": decision.composite_score, "commercial_score": assessment.commercial_score, "autonomy_score": assessment.autonomous_operability_score, "autonomous_gate_result": assessment.gate_result, "founder_dependency": assessment.founder_dependency, "minutes_per_week_human": assessment.minutes_per_week_human, "probability": decision.probability_500_within_30_days, "decision": decision.decision, "evidence_tier": decision.evidence_tier, "initial_tranche": str(decision.initial_tranche or "0"), "ai_leverage": decision.component_scores.get("AI Leverage", 0), "platformization_potential": decision.component_scores.get("Platformization Potential", 0), "portfolio_score": score, "capability_burden": capability_burden, "collision_risk": collision_risk[str(proposal.id)], "concentration_penalty": concentration_penalty}) rows.sort(key=lambda item: item["portfolio_score"], reverse=True) return rows @@ -547,9 +642,15 @@ class VentureDiscoveryService: thesis.first_seen_cohort = thesis.first_seen_cohort or cohort thesis.last_seen_cohort = cohort thesis.status = analysis.status_recommendation + best_assessment = self.assess_autonomous_operability(best[0]) if best else None + if best_assessment: + thesis.venture_track = best_assessment.venture_track + thesis.best_autonomous_operability_score = max(float(thesis.best_autonomous_operability_score or 0.0), best_assessment.autonomous_operability_score) + thesis.best_founder_dependency = best_assessment.founder_dependency + thesis.autonomous_candidate_status = AutonomousCandidateStatus.AUTONOMOUS_CANDIDATE if best_assessment.gate_result == AutonomousGateResult.AUTONOMOUS_ELIGIBLE else AutonomousCandidateStatus.ASSISTED_CANDIDATE if best_assessment.gate_result == AutonomousGateResult.ASSISTED_ONLY else AutonomousCandidateStatus.AUTONOMOUS_REJECTED if best_assessment.gate_result == AutonomousGateResult.REJECT_OPERABILITY else AutonomousCandidateStatus.NOT_ASSESSED thesis.metadata = {**thesis.metadata, "last_saturation_rationale": analysis.rationale, "last_reopen_reason": thesis.metadata.get("last_reopen_reason", "")} - thesis.save(update_fields=["proposal_count", "best_ic_score", "best_company", "first_seen_cohort", "last_seen_cohort", "status", "metadata", "updated_at"]) - updates.append({"thesis": thesis.canonical_name, "status": thesis.status, "proposal_count": thesis.proposal_count, "best_ic_score": thesis.best_ic_score}) + thesis.save(update_fields=["proposal_count", "best_ic_score", "best_company", "first_seen_cohort", "last_seen_cohort", "status", "venture_track", "best_autonomous_operability_score", "best_founder_dependency", "autonomous_candidate_status", "metadata", "updated_at"]) + updates.append({"thesis": thesis.canonical_name, "status": thesis.status, "proposal_count": thesis.proposal_count, "best_ic_score": thesis.best_ic_score, "venture_track": thesis.venture_track, "best_autonomous_operability_score": thesis.best_autonomous_operability_score, "best_founder_dependency": thesis.best_founder_dependency, "autonomous_candidate_status": thesis.autonomous_candidate_status}) cohort.metadata = {**cohort.metadata, "thesis_registry_after": self.registry_snapshot()} cohort.save(update_fields=["metadata", "updated_at"]) return updates @@ -587,15 +688,20 @@ class VentureDiscoveryService: review = cohort.portfolio_review ideation_mandate = getattr(cohort, "ideation_mandate", None) clusters = [{"company": cluster.proposal.title, "portfolio_thesis": cluster.portfolio_thesis.canonical_name if cluster.portfolio_thesis else "", "classification": cluster.classification, "similarity_score": cluster.similarity_score, "explanation": cluster.explanation} for cluster in cohort.thesis_clusters.select_related("proposal", "portfolio_thesis").order_by("created_at")] + autonomy = [self.autonomy_report_row(member.proposal) for member in cohort.members.select_related("proposal").order_by("rank", "created_at")] generation_rejections = [{"slot_index": rejection.slot_index, "attempt": rejection.attempt, "decision": rejection.decision, "reason": rejection.reason, "candidate_title": rejection.candidate.get("title", "") if isinstance(rejection.candidate, dict) else "", "similarity_score": rejection.similarity_score} for rejection in cohort.generation_rejections.order_by("slot_index", "attempt")] saturation = [{"thesis": analysis.portfolio_thesis.canonical_name, "proposal_count": analysis.proposal_count, "best_ic_score": analysis.best_ic_score, "score_spread": analysis.score_spread, "status_recommendation": analysis.status_recommendation, "rationale": analysis.rationale} for analysis in cohort.saturation_analyses.select_related("portfolio_thesis").order_by("-proposal_count")] accepted_count = cohort.members.count() - content = {"cohort_id": cohort.cohort_id, "mandate": cohort.mandate.objective, "accepted_count": accepted_count, "requested_count": cohort.cohort_size, "ideation_mandate": self.ideation_mandate_payload(ideation_mandate) if ideation_mandate else {}, "hard_exclusions": ideation_mandate.hard_exclusions if ideation_mandate else [], "soft_exclusions": ideation_mandate.soft_exclusions if ideation_mandate else [], "search_territories": ideation_mandate.opportunity_territories if ideation_mandate else [], "generation_rejections": generation_rejections, "thesis_registry_before": (ideation_mandate.registry_snapshot.get("theses", []) if ideation_mandate else []), "thesis_registry_after": cohort.metadata.get("thesis_registry_after", []), "thesis_clusters": clusters, "saturation_analysis": saturation, "idea_diversity_metrics": self.diversity_metrics(cohort), "runtime": cohort.metrics, "total_spend": 0, "customer_outreach": "none", "rankings": review.rankings, "top_3": review.top_3, "collisions": self._collision_summary(cohort), "portfolio_concentration": review.concentration, "capability_demand": review.capability_demand, "top_3_capability_gaps": review.top_3_capability_gaps, "recommended_build_priorities": review.recommended_build_priorities} + content = {"cohort_id": cohort.cohort_id, "mandate": cohort.mandate.objective, "accepted_count": accepted_count, "requested_count": cohort.cohort_size, "venture_track": VentureTrack.AUTONOMOUS, "autonomous_operability_report": autonomy, "assisted_only_companies": [row for row in autonomy if row.get("gate_result") == AutonomousGateResult.ASSISTED_ONLY], "ideation_mandate": self.ideation_mandate_payload(ideation_mandate) if ideation_mandate else {}, "hard_exclusions": ideation_mandate.hard_exclusions if ideation_mandate else [], "soft_exclusions": ideation_mandate.soft_exclusions if ideation_mandate else [], "search_territories": ideation_mandate.opportunity_territories if ideation_mandate else [], "generation_rejections": generation_rejections, "thesis_registry_before": (ideation_mandate.registry_snapshot.get("theses", []) if ideation_mandate else []), "thesis_registry_after": cohort.metadata.get("thesis_registry_after", []), "thesis_clusters": clusters, "saturation_analysis": saturation, "idea_diversity_metrics": self.diversity_metrics(cohort), "runtime": cohort.metrics, "total_spend": 0, "customer_outreach": "none", "rankings": review.rankings, "top_3": review.top_3, "collisions": self._collision_summary(cohort), "portfolio_concentration": review.concentration, "capability_demand": review.capability_demand, "top_3_capability_gaps": review.top_3_capability_gaps, "recommended_build_priorities": review.recommended_build_priorities} readable = self._readable_cohort_report(content) cohort.status = "COMPLETE" if accepted_count == cohort.cohort_size else "PARTIAL_COMPLETE" cohort.save(update_fields=["status", "updated_at"]) return VentureArtifact.objects.create(mandate=cohort.mandate, graph_run=cohort.graph_run, artifact_type="VENTURE_DISCOVERY_COHORT_REPORT", name="Venture Discovery Cohort Report", content=content, readable=readable, generated_by="Portfolio IC") + def autonomy_report_row(self, proposal: CompanyProposal) -> dict[str, Any]: + assessment = self.assess_autonomous_operability(proposal) + return {"company": proposal.title, "venture_track": assessment.venture_track, "gate_result": assessment.gate_result, "commercial_score": assessment.commercial_score, "autonomous_operability_score": assessment.autonomous_operability_score, "founder_dependency": assessment.founder_dependency, "dependency_causes": assessment.dependency_causes, "minutes_per_week_human": assessment.minutes_per_week_human, "human_actions_required": assessment.human_actions_required, "operating_loop": assessment.operating_loop, "fulfillment_contract": assessment.fulfillment_contract, "validation_offer": assessment.validation_offer, "end_state_business_model": assessment.end_state_business_model, "structural_blockers": assessment.structural_blockers, "platform_blockers": assessment.platform_blockers, "component_scores": assessment.component_scores, "rationale": assessment.rationale} + def calibrate_probability(self, proposal: CompanyProposal, *, raw_probability: float) -> dict[str, Any]: tier = proposal.evidence_tier or EvidenceTier.TIER_0_THESIS ceiling = float(EVIDENCE_CEILINGS.get(tier, 35)) @@ -912,7 +1018,7 @@ class VentureDiscoveryService: try: slot = f" Independent cohort slot: {ideation_index}. Do not use or imitate other cohort ideas; no other ideas are visible." if ideation_index else "" context = ideation_context or {} - response = self.router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, model_hint=self.ideation_model_hint, prompt="Generate exactly ONE startup idea for Venture Discovery V0.3. Return a single JSON object, not a list. Respect no spend and no outreach in V0. Keep the $50 to $500 in 30 days mandate. Prefer AI-native businesses where Artifex can deliver most value using agents/local inference, but do not allow 'uses AI' to substitute for real customer pain. Prefer AI wrapper platforms, agentic workflow products, vertical copilots, AI-enabled services with low human labor, developer/AI infrastructure, intelligence products, automation products, service-to-platform paths, recurring revenue, and owned-compute leverage. DO NOT propose companies substantially equivalent to these hard-excluded thesis areas, including semantic variants: " + json.dumps(context.get("hard_exclusions", [])) + ". Soft exclusions require material differentiation and an explicit exception rationale: " + json.dumps(context.get("soft_exclusions", [])) + ". Search territory for this slot: " + str(context.get("territory", "OPEN_CATEGORY")) + ". This is a creative search constraint, not a fixed solution. Include title, one_line_thesis, description, problem, target_customer, proposed_solution, business_model, pricing_hypothesis, acquisition_strategy, validation_plan, capital_requested, time_to_first_dollar_estimate, expected_margin, build_complexity, market_evidence, differentiation, major_risks, confidence, and optional exception_rationale." + slot + " Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets, "ai_native_policy": AI_NATIVE_POLICY, "ideation_brief": context.get("brief", {})}))) + response = self.router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, model_hint=self.ideation_model_hint, prompt="Generate exactly ONE startup idea for an AUTONOMOUS Venture Discovery cohort. Return a single JSON object, not a list. The business must be operable by Artifex with <=30 minutes/week routine human involvement. Prefer archetypes: " + json.dumps(AUTONOMOUS_ARCHETYPES) + ". Require concrete digital operating loop: discover lead/user -> qualify -> acquire -> checkout -> onboard -> fulfill -> verify -> deliver -> support -> measure -> retain/upsell. Include validation_offer and end_state_business_model as separate fields. Include fulfillment_contract with customer_input, artifex_process, customer_output, quality_verification, failure_recovery, billing_event, support_model, escalation. Avoid enterprise procurement, founder-led calls, bespoke consulting, legal/medical judgment, offline fulfillment, and custom implementation. Respect no spend and no outreach in V0. Keep the $50 to $500 in 30 days mandate. Prefer AI-native businesses where Artifex can deliver most value using agents/local inference, but do not allow 'uses AI' to substitute for real customer pain. DO NOT propose companies substantially equivalent to these hard-excluded thesis areas, including semantic variants: " + json.dumps(context.get("hard_exclusions", [])) + ". Soft exclusions require material differentiation and an explicit exception rationale: " + json.dumps(context.get("soft_exclusions", [])) + ". Search territory for this slot: " + str(context.get("territory", "OPEN_CATEGORY")) + ". Include title, one_line_thesis, description, problem, target_customer, proposed_solution, business_model, pricing_hypothesis, acquisition_strategy, validation_plan, capital_requested, time_to_first_dollar_estimate, expected_margin, build_complexity, market_evidence, differentiation, major_risks, confidence, minutes_per_week_human, human_actions_required, and optional exception_rationale." + slot + " Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets, "ai_native_policy": AI_NATIVE_POLICY, "ideation_brief": context.get("brief", {})}))) parsed = extract_json_object(response.content) if isinstance(parsed, dict) and parsed.get("title"): return self._normalize_payload(parsed), self.ideation_model_hint @@ -933,6 +1039,9 @@ class VentureDiscoveryService: normalized["major_risks"] = self._as_list(normalized.get("major_risks")) if payload.get("exception_rationale"): normalized["exception_rationale"] = payload["exception_rationale"] + for key in ["validation_offer", "end_state_business_model", "fulfillment_contract", "minutes_per_week_human", "human_actions_required"]: + if key in payload: + normalized[key] = payload[key] return normalized def _normalize_research(self, payload: dict[str, Any], required: list[str]) -> dict[str, Any]: @@ -945,7 +1054,9 @@ class VentureDiscoveryService: def _searxng_sources(self, proposal: CompanyProposal, required: list[str], *, depth: str = "light") -> list[dict[str, Any]]: client = self.search_client or SearxngSearchClient.from_resources() + self._last_search_diagnostics = {"queries": [], "errors": []} if client is None: + self._last_search_diagnostics["errors"].append("no active searxng resource") return [] sources = [] limit = 5 if depth == "deep" else 3 @@ -959,11 +1070,34 @@ class VentureDiscoveryService: for category in required: query = f"{proposal.title} {proposal.problem[:120]} {proposal.target_customer} {query_terms.get(category, category)}" try: - sources.extend(client.search(query, category=category, limit=limit)) - except Exception: + results = self._search_client_results(client, query, category=category, limit=limit) + if not results: + fallback_query = f"{proposal.title} {query_terms.get(category, category)}" + results = self._search_client_results(client, fallback_query, category=category, limit=limit) + if not results: + broad_query = f"{proposal.target_customer} {query_terms.get(category, category)}" + results = self._search_client_results(client, broad_query, category=category, limit=limit) + sources.extend(results) + except Exception as exc: + self._last_search_diagnostics["errors"].append(f"{category}: {exc}") continue return sources[:25 if depth == "deep" else 15] + def _search_client_results(self, client: Any, query: str, *, category: str, limit: int) -> list[dict[str, Any]]: + if hasattr(client, "search_payload"): + payload = client.search_payload(query) + raw_results = payload.get("results", []) if isinstance(payload, dict) else [] + unresponsive = payload.get("unresponsive_engines", []) if isinstance(payload, dict) else [] + self._last_search_diagnostics["queries"].append({"category": category, "query": query, "result_count": len(raw_results), "unresponsive_engines": unresponsive}) + return [ + {"type": "public_web", "source": "searxng", "url": str(item["url"]), "title": str(item.get("title", "")), "category": category, "summary": str(item.get("content", item.get("snippet", ""))), "fallback_evidence": False} + for item in raw_results[: limit * 4] + if isinstance(item, dict) and item.get("url") + ] + results = client.search(query, category=category, limit=limit) + self._last_search_diagnostics["queries"].append({"category": category, "query": query, "result_count": len(results)}) + return results + def _page_corpus(self, search_sources: list[dict[str, Any]]) -> list[dict[str, Any]]: if self.page_fetcher is None: return [] @@ -1024,9 +1158,13 @@ class VentureDiscoveryService: } company_score = self._jaccard(company_text, source_text) category_score = self._jaccard(category_terms.get(category, category), source_text) + company_overlap = self._token_overlap(company_text, source_text) + category_overlap = self._token_overlap(category_terms.get(category, category), source_text) if category.replace("_", " ") in source_text: category_score = max(category_score, 0.5) - score = round(company_score * 0.65 + category_score * 0.35, 2) + score = round(company_score * 0.55 + category_score * 0.25 + min(company_overlap, 5) * 0.03 + min(category_overlap, 4) * 0.025, 2) + if company_overlap >= 2 and category_overlap >= 1: + return {"accepted": True, "quality": "strong" if score >= 0.13 or category_overlap >= 2 else "weak", "score": score, "reason": "relevant proposal and category overlap"} if score < 0.08: return {"accepted": False, "quality": "weak", "score": score, "reason": "insufficient semantic relevance"} if score < 0.16: @@ -1106,7 +1244,12 @@ class VentureDiscoveryService: ai_leverage = self.ai_leverage_score(proposal) platformization = self.platformization_potential(proposal) probability = round((demand * 0.22 + pricing * 0.12 + pain * 0.16 + distribution * 0.14 + build * 0.1 + capital * 0.1 + risk * 0.16), 0) - return {"Demand Evidence": demand, "Time-to-First-Dollar Attractiveness": 76 if service_model else 50, "Capital Efficiency": capital, "Validation Affordability": validation, "Gross Margin Potential": gross_margin, "Distribution Feasibility": distribution, "Build Simplicity": build, "Defensibility": min(75, defensibility), "Market Opportunity": market_size, "Competitive Position": competition, "Risk Manageability": risk, "AI Leverage": ai_leverage, "Platformization Potential": platformization, "Probability of Reaching $500": int(max(20, min(80, probability)))} + autonomy = 55 + try: + autonomy = int(proposal.autonomous_assessment.autonomous_operability_score) + except ObjectDoesNotExist: + pass + return {"Demand Evidence": demand, "Time-to-First-Dollar Attractiveness": 76 if service_model else 50, "Capital Efficiency": capital, "Validation Affordability": validation, "Gross Margin Potential": gross_margin, "Distribution Feasibility": distribution, "Build Simplicity": build, "Defensibility": min(75, defensibility), "Market Opportunity": market_size, "Competitive Position": competition, "Risk Manageability": risk, "AI Leverage": ai_leverage, "Platformization Potential": platformization, "Autonomous Operability": autonomy, "Probability of Reaching $500": int(max(20, min(80, probability)))} def ai_leverage_score(self, proposal: CompanyProposal) -> int: text = " ".join([proposal.title, proposal.description, proposal.problem, proposal.proposed_solution, proposal.business_model, proposal.differentiation]).lower() @@ -1153,6 +1296,10 @@ class VentureDiscoveryService: def _capability_requirements(self, proposal: CompanyProposal) -> list[dict[str, Any]]: research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {} web_status = CapabilityStatus.MISSING if not self.web_research_available else CapabilityStatus.AVAILABLE if not research.get("unverified_categories") else CapabilityStatus.PARTIAL + try: + structural = proposal.autonomous_assessment.structural_blockers + except ObjectDoesNotExist: + structural = [] return [ {"category": "Company Brain", "status": CapabilityStatus.PARTIAL, "rationale": "Venture reasoning exists in V0 but is not a persistent operating brain.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}}, {"category": "Board", "status": CapabilityStatus.AVAILABLE, "rationale": "Structured CEO/CTO/CFO/CRO/Independent Director review exists for V0.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}}, @@ -1160,14 +1307,15 @@ class VentureDiscoveryService: {"category": "WEB_MARKET_RESEARCH", "status": web_status, "rationale": "Bounded source-linked web research exists only when configured and category coverage is complete.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"web_research_available": self.web_research_available, **research}}, {"category": "software build", "status": CapabilityStatus.AVAILABLE, "rationale": "Task execution, coding, review, tests, and graph runtime exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}}, {"category": "frontend design", "status": CapabilityStatus.AVAILABLE, "rationale": "Frontend agents and Django UI path exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}}, - {"category": "deployment", "status": CapabilityStatus.PARTIAL, "rationale": "Deployment planning exists, but company-specific production deployment workflow is not implemented.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}}, + {"category": "deployment", "status": CapabilityStatus.PARTIAL, "rationale": "Validation can use SUBDOMAIN_DEPLOYMENT under a shared parent domain; dedicated domains are a traction-stage upgrade.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {"deployment_model": "SUBDOMAIN_DEPLOYMENT", "domain_purchase_required_for_validation": False}}, {"category": "outbound sales", "status": CapabilityStatus.MISSING, "rationale": "No compliant outreach/sequence/customer contact system exists and V0 forbids outreach.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}}, {"category": "CRM", "status": CapabilityStatus.MISSING, "rationale": "No customer pipeline/contact tracking exists.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}}, - {"category": "payments", "status": CapabilityStatus.MISSING, "rationale": "No payment collection system exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}}, + {"category": "payments", "status": CapabilityStatus.PARTIAL, "rationale": "Stripe merchant account is available as CONFIGURED_EXTERNAL_PROVIDER, but no real charges or product checkout integration are implemented in this milestone.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {"payment_provider_state": "CONFIGURED_EXTERNAL_PROVIDER", "implementation_state": "IMPLEMENTATION_MISSING", "real_charges_allowed": False}}, {"category": "invoicing", "status": CapabilityStatus.MISSING, "rationale": "No invoicing workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}}, {"category": "customer support", "status": CapabilityStatus.MISSING, "rationale": "No support inbox or customer service workflow exists.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}}, {"category": "company budget management", "status": CapabilityStatus.MISSING, "rationale": "V0 blocks spend but future validation needs tranche/budget controls.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}}, {"category": "legal/compliance", "status": CapabilityStatus.MISSING, "rationale": "No contracts, terms, privacy, or compliance review workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}}, + {"category": "COMPANY_STRUCTURAL_HUMAN_DEPENDENCY", "status": CapabilityStatus.MISSING if structural else CapabilityStatus.AVAILABLE, "rationale": "Structural human dependency is inherent to the company model and should route assisted-only if severe; it is not solved by platform infrastructure.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"dependency_causes": structural}}, ] def _portfolio_concentration(self, cohort: VentureCohort) -> dict[str, Any]: diff --git a/control_plane/ventures/management/commands/export_venture_cohort.py b/control_plane/ventures/management/commands/export_venture_cohort.py index 6a7fdcf..c96e774 100644 --- a/control_plane/ventures/management/commands/export_venture_cohort.py +++ b/control_plane/ventures/management/commands/export_venture_cohort.py @@ -107,6 +107,32 @@ class Command(BaseCommand): "market_evidence": proposal.market_evidence, "fingerprint": self.fingerprint_payload(proposal), "decision": self.decision_payload(proposal), + "autonomous_assessment": self.autonomous_payload(proposal), + } + + def autonomous_payload(self, proposal) -> dict[str, Any] | None: + try: + assessment = proposal.autonomous_assessment + except ObjectDoesNotExist: + return None + return { + "venture_track": assessment.venture_track, + "gate_result": assessment.gate_result, + "commercial_score": assessment.commercial_score, + "autonomous_operability_score": assessment.autonomous_operability_score, + "founder_dependency": assessment.founder_dependency, + "dependency_causes": assessment.dependency_causes, + "minutes_per_week_human": assessment.minutes_per_week_human, + "human_actions_required": assessment.human_actions_required, + "human_action_categories": assessment.human_action_categories, + "operating_loop": assessment.operating_loop, + "fulfillment_contract": assessment.fulfillment_contract, + "validation_offer": assessment.validation_offer, + "end_state_business_model": assessment.end_state_business_model, + "structural_blockers": assessment.structural_blockers, + "platform_blockers": assessment.platform_blockers, + "component_scores": assessment.component_scores, + "rationale": assessment.rationale, } def fingerprint_payload(self, proposal) -> dict[str, Any] | None: @@ -182,12 +208,17 @@ class Command(BaseCommand): def company_markdown(self, company: dict[str, Any], *, include_sources: bool, max_sources: int) -> list[str]: decision = company.get("decision") or {} research = company.get("research") or {} + autonomy = company.get("autonomous_assessment") or {} lines = [ f"### Rank {company['rank']}: {company['title']}", "", "Portfolio score: " f"`{company['portfolio_score']}`. IC decision: `{decision.get('decision', 'none')}`. " f"Composite score: `{decision.get('composite_score', '')}`. " + f"Commercial score: `{autonomy.get('commercial_score', '')}`. " + f"Autonomous operability: `{autonomy.get('autonomous_operability_score', '')}`. " + f"Founder dependency: `{autonomy.get('founder_dependency', 'not_assessed')}`. " + f"Gate: `{autonomy.get('gate_result', 'not_assessed')}`. " f"P($500/30d): `{decision.get('probability_500_within_30_days', '')}%`. " f"Raw P($500/30d): `{decision.get('raw_probability_500_within_30_days', '')}%`. " f"Evidence ceiling: `{decision.get('evidence_ceiling', '')}%`. " @@ -214,6 +245,12 @@ class Command(BaseCommand): "", f"Major risks: {self.inline_list(company.get('major_risks', []))}", "", + f"Validation offer: {json.dumps(autonomy.get('validation_offer', {}), default=str)}", + "", + f"End-state business: {json.dumps(autonomy.get('end_state_business_model', {}), default=str)}", + "", + f"Human minutes/week: `{autonomy.get('minutes_per_week_human', '')}`. Structural blockers: {self.inline_list(autonomy.get('structural_blockers', []))}. Platform blockers: {self.inline_list(autonomy.get('platform_blockers', []))}.", + "", "Research: " f"Coverage `{research.get('coverage_ratio', 0)}`; sources `{research.get('source_count', 0)}`; " f"page fetches `{research.get('page_fetch_count', 0)}`; provider `{research.get('provider', 'none')}`; " diff --git a/control_plane/ventures/management/commands/venture_research_smoke.py b/control_plane/ventures/management/commands/venture_research_smoke.py new file mode 100644 index 0000000..7c4335c --- /dev/null +++ b/control_plane/ventures/management/commands/venture_research_smoke.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import json + +from django.core.management.base import BaseCommand + +from agents.venture_discovery import VentureDiscoveryService +from model_router.providers import providers_from_resources +from model_router.router import ModelRouter +from research.searxng import SearxngSearchClient + + +class Command(BaseCommand): + help = "Run a single-company Venture Research smoke test." + + def add_arguments(self, parser): + parser.add_argument("--depth", choices=["light", "deep"], default="deep") + parser.add_argument("--indent", type=int, default=2) + parser.add_argument("--searxng-url", default="") + + def handle(self, *args, **options): + providers = providers_from_resources() + search_client = SearxngSearchClient(endpoint_url=str(options["searxng_url"])) if options["searxng_url"] else None + service = VentureDiscoveryService(ModelRouter(providers), web_research_available=True, search_client=search_client) + mandate = service.create_v0_mandate() + proposal = service.generate_single_company( + mandate, + payload={ + "title": "AI Website Accessibility Regression Monitor", + "one_line_thesis": "A self-service monitor detects website accessibility regressions and sends automated prioritized reports.", + "description": "Customers submit a URL, Artifex crawls pages, detects accessibility regressions, verifies issues, and delivers a recurring report.", + "problem": "Small web teams need affordable continuous accessibility checks but cannot manually audit every change.", + "target_customer": "Small SaaS and ecommerce teams with public marketing sites.", + "proposed_solution": "Automated crawler, accessibility analysis, regression detection, report generation, and email/dashboard delivery.", + "business_model": "Self-service subscription plus a $49 validation scan.", + "pricing_hypothesis": "$49 validation scan, then $99/month subscription.", + "acquisition_strategy": "SEO/content around accessibility regression monitoring and self-service checkout.", + "validation_plan": "Offer a $49 automated scan with sample output before building a full dashboard.", + "capital_requested": "50", + "time_to_first_dollar_estimate": "1-7 days after approval", + "expected_margin": "85-95%", + "build_complexity": "LOW-MEDIUM", + "market_evidence": [], + "differentiation": "Autonomous recurring monitor with structured verification and self-service onboarding.", + "major_risks": ["Search demand may be low", "False positives may increase support"], + "confidence": 0.64, + }, + source="research_smoke", + ) + research = service.conduct_market_research(proposal, depth=str(options["depth"])) + proposal.refresh_from_db() + stored = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {} + payload = { + "proposal_id": str(proposal.id), + "title": proposal.title, + "search_result_count": stored.get("search_result_count", 0), + "page_fetch_count": stored.get("page_fetch_count", 0), + "accepted_source_count": stored.get("source_count", 0), + "rejected_source_count": stored.get("source_rejection_count", 0), + "accepted_categories": sorted({str(item.get("category", "")) for item in research.get("sources", []) if isinstance(item, dict) and item.get("url")}), + "coverage": stored.get("coverage", {}), + "coverage_ratio": stored.get("coverage_ratio", 0.0), + "provider": stored.get("provider", "none"), + "search_provider": stored.get("search_provider", "none"), + "search_diagnostics": stored.get("search_diagnostics", {}), + "persisted_url_count": len([item for item in proposal.market_evidence if isinstance(item, dict) and item.get("url")]), + "accepted_sources": research.get("sources", [])[:5], + "rejected_sources": research.get("source_rejections", [])[:5], + "explicit_research_failure": stored.get("explicit_research_failure", ""), + } + indent = None if int(options["indent"]) <= 0 else int(options["indent"]) + self.stdout.write(json.dumps(payload, indent=indent, default=str)) diff --git a/control_plane/ventures/migrations/0004_portfoliothesis_autonomous_candidate_status_and_more.py b/control_plane/ventures/migrations/0004_portfoliothesis_autonomous_candidate_status_and_more.py new file mode 100644 index 0000000..c9c7be2 --- /dev/null +++ b/control_plane/ventures/migrations/0004_portfoliothesis_autonomous_candidate_status_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.16 on 2026-08-16 10:24 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ventures', '0003_cohortideationmandate_portfoliothesis_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='portfoliothesis', + name='autonomous_candidate_status', + field=models.CharField(choices=[('NOT_ASSESSED', 'Not Assessed'), ('AUTONOMOUS_CANDIDATE', 'Autonomous Candidate'), ('ASSISTED_CANDIDATE', 'Assisted Candidate'), ('AUTONOMOUS_REJECTED', 'Autonomous Rejected')], default='NOT_ASSESSED', max_length=32), + ), + migrations.AddField( + model_name='portfoliothesis', + name='best_autonomous_operability_score', + field=models.FloatField(default=0.0), + ), + migrations.AddField( + model_name='portfoliothesis', + name='best_founder_dependency', + field=models.CharField(choices=[('NONE', 'None'), ('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('CRITICAL', 'Critical')], default='MEDIUM', max_length=32), + ), + migrations.AddField( + model_name='portfoliothesis', + name='venture_track', + field=models.CharField(choices=[('AUTONOMOUS', 'Autonomous'), ('ASSISTED', 'Assisted')], default='AUTONOMOUS', max_length=32), + ), + migrations.CreateModel( + name='AutonomousOperabilityAssessment', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('venture_track', models.CharField(choices=[('AUTONOMOUS', 'Autonomous'), ('ASSISTED', 'Assisted')], default='AUTONOMOUS', max_length=32)), + ('gate_result', models.CharField(choices=[('AUTONOMOUS_ELIGIBLE', 'Autonomous Eligible'), ('AUTONOMOUS_BORDERLINE', 'Autonomous Borderline'), ('ASSISTED_ONLY', 'Assisted Only'), ('REJECT_OPERABILITY', 'Reject Operability')], max_length=40)), + ('autonomous_operability_score', models.FloatField(default=0.0)), + ('commercial_score', models.FloatField(default=0.0)), + ('founder_dependency', models.CharField(choices=[('NONE', 'None'), ('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('CRITICAL', 'Critical')], max_length=32)), + ('dependency_causes', models.JSONField(blank=True, default=list)), + ('minutes_per_week_human', models.PositiveIntegerField(default=30)), + ('human_actions_required', models.JSONField(blank=True, default=list)), + ('human_action_categories', models.JSONField(blank=True, default=list)), + ('operating_loop', models.JSONField(blank=True, default=dict)), + ('fulfillment_contract', models.JSONField(blank=True, default=dict)), + ('validation_offer', models.JSONField(blank=True, default=dict)), + ('end_state_business_model', models.JSONField(blank=True, default=dict)), + ('structural_blockers', models.JSONField(blank=True, default=list)), + ('platform_blockers', models.JSONField(blank=True, default=list)), + ('component_scores', models.JSONField(blank=True, default=dict)), + ('rationale', models.TextField(blank=True)), + ('metadata', models.JSONField(blank=True, default=dict)), + ('proposal', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='autonomous_assessment', to='ventures.companyproposal')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/control_plane/ventures/models.py b/control_plane/ventures/models.py index 18c1231..9e16aae 100644 --- a/control_plane/ventures/models.py +++ b/control_plane/ventures/models.py @@ -91,6 +91,33 @@ class OpportunityTerritory(models.TextChoices): OPEN_CATEGORY = "OPEN_CATEGORY" +class VentureTrack(models.TextChoices): + AUTONOMOUS = "AUTONOMOUS" + ASSISTED = "ASSISTED" + + +class FounderDependencyLevel(models.TextChoices): + NONE = "NONE" + LOW = "LOW" + MEDIUM = "MEDIUM" + HIGH = "HIGH" + CRITICAL = "CRITICAL" + + +class AutonomousGateResult(models.TextChoices): + AUTONOMOUS_ELIGIBLE = "AUTONOMOUS_ELIGIBLE" + AUTONOMOUS_BORDERLINE = "AUTONOMOUS_BORDERLINE" + ASSISTED_ONLY = "ASSISTED_ONLY" + REJECT_OPERABILITY = "REJECT_OPERABILITY" + + +class AutonomousCandidateStatus(models.TextChoices): + NOT_ASSESSED = "NOT_ASSESSED" + AUTONOMOUS_CANDIDATE = "AUTONOMOUS_CANDIDATE" + ASSISTED_CANDIDATE = "ASSISTED_CANDIDATE" + AUTONOMOUS_REJECTED = "AUTONOMOUS_REJECTED" + + class CompanyMandate(TimestampedModel): objective = models.TextField() constraints = models.JSONField(default=dict, blank=True) @@ -159,6 +186,10 @@ class PortfolioThesis(TimestampedModel): first_seen_cohort = models.ForeignKey("VentureCohort", on_delete=models.SET_NULL, null=True, blank=True, related_name="first_seen_portfolio_theses") last_seen_cohort = models.ForeignKey("VentureCohort", on_delete=models.SET_NULL, null=True, blank=True, related_name="last_seen_portfolio_theses") metadata = models.JSONField(default=dict, blank=True) + venture_track = models.CharField(max_length=32, choices=VentureTrack.choices, default=VentureTrack.AUTONOMOUS) + best_autonomous_operability_score = models.FloatField(default=0.0) + best_founder_dependency = models.CharField(max_length=32, choices=FounderDependencyLevel.choices, default=FounderDependencyLevel.MEDIUM) + autonomous_candidate_status = models.CharField(max_length=32, choices=AutonomousCandidateStatus.choices, default=AutonomousCandidateStatus.NOT_ASSESSED) class CohortIdeationMandate(TimestampedModel): @@ -208,6 +239,28 @@ class PortfolioSaturationAnalysis(TimestampedModel): metadata = models.JSONField(default=dict, blank=True) +class AutonomousOperabilityAssessment(TimestampedModel): + proposal = models.OneToOneField(CompanyProposal, on_delete=models.CASCADE, related_name="autonomous_assessment") + venture_track = models.CharField(max_length=32, choices=VentureTrack.choices, default=VentureTrack.AUTONOMOUS) + gate_result = models.CharField(max_length=40, choices=AutonomousGateResult.choices) + autonomous_operability_score = models.FloatField(default=0.0) + commercial_score = models.FloatField(default=0.0) + founder_dependency = models.CharField(max_length=32, choices=FounderDependencyLevel.choices) + dependency_causes = models.JSONField(default=list, blank=True) + minutes_per_week_human = models.PositiveIntegerField(default=30) + human_actions_required = models.JSONField(default=list, blank=True) + human_action_categories = models.JSONField(default=list, blank=True) + operating_loop = models.JSONField(default=dict, blank=True) + fulfillment_contract = models.JSONField(default=dict, blank=True) + validation_offer = models.JSONField(default=dict, blank=True) + end_state_business_model = models.JSONField(default=dict, blank=True) + structural_blockers = models.JSONField(default=list, blank=True) + platform_blockers = models.JSONField(default=list, blank=True) + component_scores = models.JSONField(default=dict, blank=True) + rationale = models.TextField(blank=True) + metadata = models.JSONField(default=dict, blank=True) + + class CompanyBoardReview(TimestampedModel): proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="board_reviews") observations = models.JSONField(default=dict, blank=True) diff --git a/graph/venture_cohort.py b/graph/venture_cohort.py index edd4cce..258a16e 100644 --- a/graph/venture_cohort.py +++ b/graph/venture_cohort.py @@ -8,8 +8,8 @@ from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec def venture_discovery_cohort_graph_v1() -> ExecutionGraphSpec: - nodes = ["prepare_cohort", "portfolio_thesis_review", "create_ideation_mandate", "allocate_search_territories", "generate_independent_proposals", "novelty_gate", "initial_research", "fingerprint_theses", "collision_analysis", "cluster_theses", "run_individual_diligence", "portfolio_compare", "portfolio_ic", "saturation_analysis", "update_thesis_registry", "aggregate_capabilities", "produce_cohort_report", "complete"] - spec = ExecutionGraphSpec(name="venture_discovery_cohort", version=3, graph_type="VENTURE_DISCOVERY_COHORT", entry="prepare_cohort", nodes={node: GraphNodeSpec(node, node if node == "complete" else f"venture_cohort_{node}") for node in nodes}, edges=[GraphEdgeSpec(nodes[index], nodes[index + 1], "success") for index in range(len(nodes) - 1)], terminal_nodes=["complete"], metadata={"description": "Venture Discovery V0.4: concurrent replacement ideation, source-quality filtering, finalist deep research, rescore, and hardened completion status."}) + nodes = ["prepare_cohort", "portfolio_thesis_review", "create_ideation_mandate", "allocate_search_territories", "generate_independent_proposals", "novelty_gate", "initial_research", "fingerprint_theses", "collision_analysis", "cluster_theses", "run_individual_diligence", "autonomous_operability_review", "portfolio_compare", "portfolio_ic", "saturation_analysis", "update_thesis_registry", "aggregate_capabilities", "produce_cohort_report", "complete"] + spec = ExecutionGraphSpec(name="venture_discovery_cohort", version=4, graph_type="VENTURE_DISCOVERY_COHORT", entry="prepare_cohort", nodes={node: GraphNodeSpec(node, node if node == "complete" else f"venture_cohort_{node}") for node in nodes}, edges=[GraphEdgeSpec(nodes[index], nodes[index + 1], "success") for index in range(len(nodes) - 1)], terminal_nodes=["complete"], metadata={"description": "Venture Discovery V0.5: autonomous operability track, research smoke hardening, finalist deep research, and autonomous-only finalists."}) spec.validate() return spec @@ -105,6 +105,13 @@ class DiligenceNode(CohortNode): return NodeResult("COMPLETE", "success", {"diligence_count": cohort.members.count()}) +class AutonomousOperabilityNode(CohortNode): + def run(self, context: GraphExecutionContext) -> NodeResult: + assessments = self.service.assess_autonomous_operability_for_cohort(self.cohort(context)) + eligible = len([item for item in assessments if item.gate_result == "AUTONOMOUS_ELIGIBLE"]) + return NodeResult("COMPLETE", "success", {"assessment_count": len(assessments), "autonomous_eligible_count": eligible}) + + class PortfolioNode(CohortNode): def run(self, context: GraphExecutionContext) -> NodeResult: review = self.service.portfolio_ic(self.cohort(context)) @@ -144,6 +151,6 @@ class NoopNode(CohortNode): def venture_discovery_cohort_registry(service: VentureDiscoveryService, *, cohort_size: int = 10, concurrency: int = 1) -> NodeHandlerRegistry: registry = NodeHandlerRegistry() - for handler in [PrepareCohortNode(service, "venture_cohort_prepare_cohort", cohort_size=cohort_size, concurrency=concurrency), PortfolioThesisReviewNode(service, "venture_cohort_portfolio_thesis_review"), IdeationMandateNode(service, "venture_cohort_create_ideation_mandate"), SearchTerritoryNode(service, "venture_cohort_allocate_search_territories"), GenerateIndependentNode(service, "venture_cohort_generate_independent_proposals"), NoveltyGateNode(service, "venture_cohort_novelty_gate"), ResearchNode(service, "venture_cohort_initial_research"), FingerprintNode(service, "venture_cohort_fingerprint_theses"), CollisionNode(service, "venture_cohort_collision_analysis"), ClusterThesesNode(service, "venture_cohort_cluster_theses"), DiligenceNode(service, "venture_cohort_run_individual_diligence"), NoopNode(service, "venture_cohort_portfolio_compare"), PortfolioNode(service, "venture_cohort_portfolio_ic"), SaturationNode(service, "venture_cohort_saturation_analysis"), RegistryUpdateNode(service, "venture_cohort_update_thesis_registry"), CapabilityNode(service, "venture_cohort_aggregate_capabilities"), ReportNode(service, "venture_cohort_produce_cohort_report")]: + for handler in [PrepareCohortNode(service, "venture_cohort_prepare_cohort", cohort_size=cohort_size, concurrency=concurrency), PortfolioThesisReviewNode(service, "venture_cohort_portfolio_thesis_review"), IdeationMandateNode(service, "venture_cohort_create_ideation_mandate"), SearchTerritoryNode(service, "venture_cohort_allocate_search_territories"), GenerateIndependentNode(service, "venture_cohort_generate_independent_proposals"), NoveltyGateNode(service, "venture_cohort_novelty_gate"), ResearchNode(service, "venture_cohort_initial_research"), FingerprintNode(service, "venture_cohort_fingerprint_theses"), CollisionNode(service, "venture_cohort_collision_analysis"), ClusterThesesNode(service, "venture_cohort_cluster_theses"), DiligenceNode(service, "venture_cohort_run_individual_diligence"), AutonomousOperabilityNode(service, "venture_cohort_autonomous_operability_review"), NoopNode(service, "venture_cohort_portfolio_compare"), PortfolioNode(service, "venture_cohort_portfolio_ic"), SaturationNode(service, "venture_cohort_saturation_analysis"), RegistryUpdateNode(service, "venture_cohort_update_thesis_registry"), CapabilityNode(service, "venture_cohort_aggregate_capabilities"), ReportNode(service, "venture_cohort_produce_cohort_report")]: registry.register(handler) return registry diff --git a/research/searxng.py b/research/searxng.py index d24b48e..f950888 100644 --- a/research/searxng.py +++ b/research/searxng.py @@ -31,11 +31,7 @@ class SearxngSearchClient: return "UNAVAILABLE" def search(self, query: str, *, category: str = "general", limit: int = 5) -> list[dict[str, Any]]: - params = urllib.parse.urlencode({"q": query, "format": "json", "categories": "general", "language": "en"}) - url = self.endpoint_url.rstrip("/") + "/search?" + params - request = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") - with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response: - payload = json.loads(response.read().decode("utf-8")) + payload = self.search_payload(query) results = [] for item in payload.get("results", [])[:limit]: if not isinstance(item, dict) or not item.get("url"): @@ -53,6 +49,13 @@ class SearxngSearchClient: ) return results + def search_payload(self, query: str) -> dict[str, Any]: + params = urllib.parse.urlencode({"q": query, "format": "json", "categories": "web", "language": "en"}) + url = self.endpoint_url.rstrip("/") + "/search?" + params + request = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response: + return json.loads(response.read().decode("utf-8")) + @dataclass class WebPageFetcher: diff --git a/tests/test_venture_discovery_cohort_v02.py b/tests/test_venture_discovery_cohort_v02.py index 17f0297..0db4c1b 100644 --- a/tests/test_venture_discovery_cohort_v02.py +++ b/tests/test_venture_discovery_cohort_v02.py @@ -172,7 +172,7 @@ def test_cohort_size_ranking_top3_capability_aggregation_and_graph_lineage() -> assert graph_run.status == GraphRunStatus.COMPLETE assert cohort.members.count() == 10 assert CompanyProposal.objects.count() == 10 - assert cohort.members.filter(is_top_3=True).count() == 3 + assert cohort.members.filter(is_top_3=True).count() == len(cohort.portfolio_review.top_3) assert PortfolioICReview.objects.filter(cohort=cohort).exists() assert VentureCollision.objects.filter(cohort=cohort).count() == 45 assert VentureCapabilityDemand.objects.filter(cohort=cohort).exists() @@ -187,7 +187,7 @@ def test_cohort_size_ranking_top3_capability_aggregation_and_graph_lineage() -> assert "ranking_changes_after_deep_research" in cohort.metrics report = cohort.mandate.artifacts.get(artifact_type="VENTURE_DISCOVERY_COHORT_REPORT") assert len(report.content["rankings"]) == 10 - assert len(report.content["top_3"]) == 3 + assert len(report.content["top_3"]) <= 3 assert report.content["total_spend"] == 0 assert CohortIdeationMandate.objects.filter(cohort=cohort).exists() assert PortfolioThesisCluster.objects.filter(cohort=cohort).count() == 10 @@ -332,7 +332,7 @@ def test_run_venture_cohort_management_command_outputs_summary(monkeypatch) -> N assert summary["concurrency"] == 2 assert summary["fallback_count"] == 0 assert summary["generation_sources"] == ["qwen"] - assert len(summary["top_3"]) == 3 + assert len(summary["top_3"]) <= 3 assert cohort.members.count() == 3 @@ -354,6 +354,8 @@ def test_export_venture_cohort_management_command_outputs_all_companies(tmp_path assert "## All Company Details" in markdown assert markdown.count("### Rank") == 3 assert "Validation condition:" in markdown + assert "Autonomous operability:" in markdown assert payload["cohort"]["cohort_id"] == cohort.cohort_id assert len(payload["companies"]) == 3 assert payload["companies"][0]["decision"]["decision"] + assert payload["companies"][0]["autonomous_assessment"]["venture_track"] diff --git a/tests/test_venture_discovery_v0.py b/tests/test_venture_discovery_v0.py index bec2f99..4a088d9 100644 --- a/tests/test_venture_discovery_v0.py +++ b/tests/test_venture_discovery_v0.py @@ -59,7 +59,7 @@ class ResearchProvider(ModelProvider): model="luna", content=json.dumps( { - "sources": [{"url": f"https://example.com/{category}", "title": category.replace("_", " ").title(), "category": category, "summary": f"Source-linked evidence for {category}."} for category in categories], + "sources": [{"url": f"https://example.com/{category}", "title": category.replace("_", " ").title(), "category": category, "summary": f"Source-linked evidence for LaunchLens validation audit serving solo technical founders with pricing, competitors, customer pain, market alternatives, regulatory compliance, tools, services, and subscription cost data for {category}."} for category in categories], "findings": {category: f"Finding for {category}." for category in categories}, "coverage": {category: category in categories for category in ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]}, }