
    ;iE                       d Z ddlZddlZddlZddlZddlZddlmZmZ ddlm	Z	 ddl
mZmZmZmZ ddlZddlmZ ddlmZmZmZmZmZmZ  G d d	ej4                        Zd
eeef   defdZd
eeef   defdZdee   dee   fdZ  e         e	e!      jD                  dz  Z#e#dz  Z$d?dede	fdZ%d?dedee	   fdZ&d?dedeeef   fdZ'	 d@deeef   deeeef      de(deeeef      fdZ)d Z*d Z+dee,   fdZ-	 	 	 dAde,de,dedeeeef      fdZ.d eeeef      deeef   fd!Z/d"eeef   deeef   fd#Z0dBd$eeef   ded%e,de(deeef   f
d&Z1d'Z2d(Z3d)Z4de(fd*Z5dCd+eeef   d,ed-e,defd.Z6d?d/ed0edee   fd1Z7d2edeeeef      fd3Z8	 	 	 dDd4eeef   ded5e,d6e(deeef   f
d7Z9d?d8edede(fd9Z:dEd:e,defd;Z;d4eeef   fd<Z<d= Z=e>d>k(  r e=        yy)Fa  
Multi-Layer Trend Analysis Pipeline

Layer 1: EMBED + CLUSTER (local) - sentence-transformers, UMAP, HDBSCAN
Layer 2: FAST LLM CLASSIFICATION (Groq Llama 3.3 70B)
Layer 3: DEEP ANALYSIS (Claude CLI via Max subscription)
Layer 3.5: DEEP RESEARCH (Hybrid Research Service API)

Outputs:
- output/analysis_clusters.json
- output/analysis_classified.json
- output/analysis_opportunities.json (includes research data)
    N)datetimetimezone)Path)ListDictAnyOptional)load_dotenv)run_migrationcreate_analysis_runupdate_analysis_runinsert_opportunities_batch&update_opportunity_research_by_clusterget_latest_analysis_runc                   "     e Zd ZdZ fdZ xZS )NumpyEncoderz&JSON encoder that handles numpy types.c                 
   t        |t        j                        rt        |      S t        |t        j                        rt        |      S t        |t        j                        r|j                         S t        | %  |      S N)

isinstancenpintegerintfloatingfloatndarraytolistsuperdefault)selfobj	__class__s     1/home/sietch6/trending-topics-pipeline/analyze.pyr   zNumpyEncoder.default)   s[    c2::&s8Oc2;;':c2::&::<ws##    )__name__
__module____qualname____doc__r   __classcell__)r!   s   @r"   r   r   '   s    0$ $r#   r   analysisreturnc                 l   ddl }| j                  d      }t        |t        t        f      r|dkD  rt	        |      S t        |t
              r5dD ]0  }||v st        ||   t        t        f      s"t	        ||         c S  t        |t              r	 t	        |      }|dkD  r|S 	 d|v r4	 t	        |j                  d      d   j                               }|dkD  r|S 	 |j                  d|      }|r)	 t	        |j                  d            }d|cxk  rdk  r|S  d	D ]  }| j                  |      }|st        |t        t        f      r|dkD  rt	        |      c S t        |t              sP|j                  d
|      }|se	 t	        |j                  d            }|dkD  r|c S  | j                  di       }|rddddddd}	ddddddd}
ddddddd}d|v r|	}n	d|v r|
}n|}d}d}|j                         D ]e  \  }}|j                  |i       }t        |t
              r|j                  dd      }nt        |t        t        f      r|}nd}|dkD  s\d}|||z  z  }g |r|dkD  rt        |d      S y# t        $ r Y w xY w# t        $ r Y w xY w# t        $ r Y w xY w# t        $ r Y w xY w)an  
    Robustly extract weighted_score from Claude's response.
    Handles various formats Claude might return:
    - Direct float: 4.65
    - String: "4.65"
    - String with denominator: "4.65/5.0"
    - Dict: {"score": 4.65} or {"value": 4.65}
    - Narrative: "The weighted score is 4.65"
    Falls back to calculating from dimension scores if all else fails.
    r   Nweighted_score)scorevaluetotalr,   /z!(\d+\.?\d*)\s*(?:/\s*5|out of 5)?      )weighted_score_calculationweighted_score_mathfinal_scoretotal_scorez=\s*(\d+\.?\d*)scoresg      ?333333?皙?)scroll_stop_powercompletion_probabilityshare_save_potentialdemand_signalvisual_potentialevergreen_potentialg333333?皙?)curiosity_intensityr=   content_gapdepth_explainabilityr?   audience_breadth)r=   rB   explainabilityr?   rD   competition_gapr:   rA           Fr-   T   )regetr   r   r   dictstr
ValueErrorsplitstripsearchgroupitemsround)r)   rI   rawkeyvalmatch	alt_fieldalt_rawr7   dimension_map_v4dimension_map_v3dimension_map_v1
active_mapr/   	found_anydim_nameweightdim_data	dim_scores                      r"   extract_weighted_scorerc   3   s"    
,,'
(C #U|$qSz #t@ 	'CczjSC<@SX&	'
 #s	*CQw
  #:CIIcN1-33567J  		>DEKKN+s<a<J   i 	,,y)'C<0Wq[W~%'3'		"4g>#EKKN37#&J #" \\(B'F "&&*$(! $#'
 $(!$(#' $
 ""#' $#
 &()J"f,)J)J	 * 0 0 2 	,Hfzz(B/H(D)$LL!4	HsEl3$		1} 	V++	, ?" A  		    " & sN   I6 *1J 2&J J  J&6	JJ	JJ	J#"J#&	J32J3c                    | j                  dd      }|dk7  r|S | j                  di       j                  dd      }|dk(  rrg d}t        fd|D              }t        |       }|d	k  s|d
k  rt        d| d| d       yy)z
    Validate and potentially correct the verdict based on MAKE_NOW gates.

    MAKE_NOW requires ALL 6 gates to pass AND weighted_score >= 4.0.
    If verdict is MAKE_NOW but gates don't all pass, downgrade to WORTH_MAKING.
    verdictSKIPMAKE_NOWmake_now_gatesgates_passedr   )scroll_stop_testuniversal_access_test
depth_test
share_testsatisfaction_testchannel_fit_testc              3   J   K   | ]  }j                  |      d u sd  yw)Tr1   NrJ   ).0ggatess     r"   	<genexpr>z#validate_verdict.<locals>.<genexpr>   s      IEIIaLD4H1Is   ##   g      @z<    [VALIDATE] Downgrading MAKE_NOW to WORTH_MAKING (gates: z/6, score: )WORTH_MAKING)rJ   sumrc   print)r)   re   ri   
gate_namesr,   rt   s        @r"   validate_verdictr|      s     ll9f-G * LL)2.E99^Q/L qU

 IjII ,H5N a>C/L\NZefteuuvwxr#   opportunitiesc                    | D cg c]  }|j                  d      dk(  s| }}| D cg c]  }|j                  d      dk7  s| }}|j                  d d       t        |      }|dk(  r| S t        dt	        |dz              }|t        dt	        |d	z              z   }|t        dt	        |d
z              z   }d}t        |      D ]K  \  }	}
|
j                  dd      }|	|k  rd}n|	|k  rd}n
|	|k  rd}nd}||k7  s8d|
d<   ||
d<   ||
d<   |dz  }M t        d| d       t        d| d||z
   d||z
   d||z
  t        |      z           ||z   S c c}w c c}w )u  
    Reassign verdicts based on relative ranking rather than
    absolute scores. This enforces the target distribution
    mechanically.

    Distribution:
    - Top 15% by weighted_score → MAKE_NOW
    - Next 35% → WORTH_MAKING
    - Next 20% → NEEDS_RESEARCH
    - Bottom 30% → SKIP

    Exception: Any opportunity the model already marked SKIP
    stays SKIP regardless of score (the model identified
    a structural reason it shouldn't be produced).
    re   rf   c                     t        |       S r   rc   xs    r"   <lambda>z)assign_verdicts_by_rank.<locals>.<lambda>   s    !7!: r#   TrU   reverser   r1   r8   gffffff?r@   UNKNOWNrg   rx   NEEDS_RESEARCHverdict_adjustedoriginal_verdictz    [RANK] Adjusted z# verdicts based on relative rankingz"    [RANK] Distribution: MAKE_NOW=z, WORTH_MAKING=z, NEEDS_RESEARCH=z, SKIP=)rJ   sortlenmaxr   	enumeraterz   )r}   omodel_skips	scoreablenmake_now_cutoffworth_making_cutoffneeds_research_cutoffadjusted_countiopporiginalnew_verdicts                r"   assign_verdicts_by_rankr      s   " ,JquuY/?6/I1JKJ)HqQUU9-=-GHIH NN:DNIIAAv !ST]+O)C3q4x=,AA/#aQX2GGNI&  3779i0$K$$(K&&*K K ("&*C"#&.C"#(C	NaN! $ 
  00S
TU	..?ObetOtNu  vG  H]  `s  Hs  Gt  t{  |}  @U  |U  X[  \g  Xh  |h  {i  j  k{""K KHs   EEEEoutputztrends_history.db
channel_idc                 2    | rt         d|  dz  S t         dz  S )a  
    Get the path for opportunities JSON file.

    Args:
        channel_id: If specified, returns channel-specific path.
                   If None, returns the generic path (backward compatibility).

    Returns:
        Path to the opportunities JSON file
    analysis_opportunities_z.jsonzanalysis_opportunities.json)
OUTPUT_DIRr   s    r"   get_opportunities_pathr     s(     5j\GGG555r#   c                     ddl }t        |       }|j                         sy|j                  d      }|j	                  ||       |S )z
    Create a backup of the existing opportunities file before writing.

    Args:
        channel_id: Channel ID for channel-specific file

    Returns:
        Path to backup file, or None if no file existed to backup
    r   Nz	.json.bak)shutilr   existswith_suffixcopy2)r   r   source_pathbackup_paths       r"   backup_opportunities_filer   +  sD     (4K))+6K
LLk*r#   c                     t        |       }|j                         r+t        |d      5 }t        j                  |      cddd       S g | dS # 1 sw Y   xY w)z
    Load existing opportunities file if it exists.

    Args:
        channel_id: Channel ID for channel-specific file

    Returns:
        Existing data dict, or empty structure if file doesn't exist
    rN)r}   r   )r   r   openjsonload)r   pathfs      r"   load_existing_opportunitiesr   @  sR     "*-D{{}$_ 	 99Q<	  	 z::	  	 s   AAexisting_datanew_opportunitiesreplace_modec                 b   |r|S | j                  dg       }i }|D ]  }|j                  d      }||||<    t               }|D ]'  }|j                  d      }||j                  |       ) g }|D ]  }	|	j                  d      }|e||v ra||   }
|	j                         }g d}|D ]  }||
v s||vs|
|   ||<    g d}|D ]  }||
v s||vs|
|   ||<    |j	                  |       {|j	                  |	        |j                         D ]  \  }}
||vs|j	                  |
        |S )a  
    Merge new opportunities with existing ones.

    Matching is done by cluster_id. New analysis updates existing entries.
    Opportunities not in the new batch are preserved.

    Args:
        existing_data: Existing opportunities data structure
        new_opportunities: Newly analyzed opportunities
        replace_mode: If True, completely replace (old behavior). If False, merge.

    Returns:
        Merged list of opportunities
    r}   
cluster_id)	research_completedresearch_report_pathresearch_report_contentresearch_word_countresearch_source_countresearch_duration_secondsresearch_generated_atresearch_queryresearch_error)video_conceptslayer4a_metadatalayer4a_errorvideo_scriptproduction_speclayer4b_metadatalayer4b_errorvisual_directionasset_specslayer4c_metadatalayer4c_error)rJ   setaddcopyappendrR   )r   r   r   existing_oppsexisting_by_clusterr   r   new_cluster_idsmergednew_oppexisting_opp
merged_oppresearch_fieldsfieldvideo_fieldss                  r"   merge_opportunitiesr   Q  s   &   !%%or:M  2WW\*
!.1
+2 eO  ,WW\*
!
+, F$ #[[.
!j4G&G.z:L JO
 ) <L(U*-D(4U(;Ju%<
L
 & <L(U*-D(4U(;Ju%< MM*%MM'";#@ %8$=$=$? ( 
L_,MM,'( Mr#   c                      g d} g }| D ]  }	 t        |        |r/t        d|        t        d       t	        j
                  d       yy# t        $ r |j                  |       Y _w xY w)z+Verify all required packages are available.)sentence_transformersumaphdbscangroqz[ERROR] Missing packages: z>Run: pip install sentence-transformers umap-learn hdbscan groqr1   N)
__import__ImportErrorr   rz   sysexit)requiredmissingpkgs      r"   check_dependenciesr     sv    H G  	 sO  *7)45NO   	 NN3	 s   AA)(A)c                     t        j                  d      } | s t        d       t        j                  d       	 t        j                  g ddddd      }|j                  d	k7  r5t        d
       t        |j                         t        j                  d       t        d       y# t        $ r# t        d       t        j                  d       Y 7t
        j                  $ r# t        d       t        j                  d       Y kw xY w)zVerify API keys are configured.GROQ_API_KEYz:[ERROR] GROQ_API_KEY not found in environment or .env filer1   )claude--print-pzSay OK TZ   inputcapture_outputtexttimeoutr   z[ERROR] Claude CLI not workingzT[ERROR] Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-codez[ERROR] Claude CLI timed outz[OK] All API keys verifiedN)osgetenvrz   r   r   
subprocessrun
returncodestderrFileNotFoundErrorTimeoutExpired)groq_keyresults     r"   check_api_keysr     s    yy(HJK1dB

 !23&-- HHQK 

&'  de$$ ,-s   A B% %)D2DDc                      t        j                  t              } | j                         }|j	                  d       |j                         }| j                          |r|d   S dS )z&Get the most recent collection run ID.z#SELECT MAX(id) FROM collection_runsr   N)sqlite3connectDB_PATHcursorexecutefetchoneclose)connr  r   s      r"   get_last_run_idr    sN    ??7#D[[]F
NN89__FJJL6!9(D(r#   limitsince_run_idc                    t        j                  t              }t         j                  |_        |j                         }|r?d}|g}|r|dz  }|j                  |       |dz  }| r|d|  z  }|j                  ||       n,d}|r|d| z  }|dz  }| r|d|  z  }|j                  |       |j                         }|j                          g }t               }	|D ]  }
|
d   j                         }|j                         |	vs)|	j                  |j                                |j                  ||
d	   |
d
   |
d   xs d|
d   xs d|
d   xs d|
d   xs d|
d   xs d|
d   xs d|
d   xs d|
d   d        |S )a  Load trend items from SQLite database.

    Args:
        limit: Maximum number of items to load
        since_run_id: If set, only load items from runs >= this ID (for incremental analysis)
        channel_id: If set, only load items tagged to this channel
    a  
            SELECT DISTINCT ti.topic_name, ti.source, ti.region, ti.final_score,
                   ti.selftext, ti.num_comments, ti.subreddit, ti.category, ti.url,
                   ti.signal_type, ti.run_id, ic.confidence as channel_confidence
            FROM trend_items ti
            JOIN item_channels ic ON ti.id = ic.item_id
            WHERE ti.topic_name IS NOT NULL AND ti.topic_name != ''
            AND ic.channel_id = ?
        z AND ti.run_id >= ?z1 ORDER BY ic.confidence DESC, ti.final_score DESCz LIMIT a  
            SELECT DISTINCT topic_name, source, region, final_score,
                   selftext, num_comments, subreddit, category, url,
                   signal_type, run_id
            FROM trend_items
            WHERE topic_name IS NOT NULL AND topic_name != ''
        z AND run_id >= z ORDER BY final_score DESC
topic_namesourceregionr5   r   selftextr   num_comments	subredditcategoryurlsignal_typeunknownrun_id)r  r  r  r-   r  r  r  r  r  r  r  )r   r   r  Rowrow_factoryr  r   r  fetchallr  r   rO   lowerr   )r  r	  r   r  r  queryparamsrowstrendsseen_topicsrowtopics               r"   load_trends_from_dbr!    s    ??7#D{{D[[]F **EMM,'DDwug&&Euf% |n55E--wug&&Eu??DJJLF%K L!'');;=+OOEKKM*MM#h-h-]+0q
O1r #N 3 8q -3
O1r5z'R"=1>Yh- 	$ Mr#   r  c           
      H   ddl m} ddl}ddl}t	        d       t	        d       t	        d       g }| D ]+  }|d   }|d   r|d	|d   dd
 z   z  }|j                  |       - t	        dt        |       d        |d      }|j                  |d      }t	        d|j                          t	        d       |j                  ddddd      }	|	j                  |      }
t	        d|
j                          t	        d       |j                  dddd      }|j                  |
      }t        t        |            d|v rd ndz
  }t        |      j                  d      }t	        d!| d"| d#       i }t!        t#        | |            D ]  \  }\  }}t%        |      }||vr$|g g t               t               t               d$||<   |d   |d%   |d&   |d'   |d(   |j'                  d)d*      d+}||   d,   j                  |       ||   d-   j)                  |j'                  d)d*             |d%   d.k(  r:||   d/   j                  |d   |d0   |d1   |d'   |d   r|d   dd2 nd3|d(   d4       ||   d5   j)                  |d%          ||   d6   j)                  |d&           |j+                         D ]  }t        |d5         |d5<   t        |d6         |d6<   t        |d-         |d-<   t        |d,         |d7<   t        |d5         |d8<   t        |d/         |d9<   t-        d: |d,   D              }||d;<   |dkD  |d<<    t/        |j+                         D cg c]  }|d=   dk7  s| c}d> d?      }d@|v r|d@   }d|d=<   dA|dB<   t        |       |||t1        j2                  t4        j6                        j9                         dC}t:        dDz  }t=        |dE      5 }t?        j@                  ||dFtB        G       ddd       t	        dH|        |S c c}w # 1 sw Y   xY w)Izc
    Layer 1: Embed topics with sentence-transformers, reduce with UMAP, cluster with HDBSCAN.
    r   )SentenceTransformerN=
============================================================zLAYER 1: EMBEDDING + CLUSTERING<============================================================r  r      z[1.1] Embedding z  topics with all-MiniLM-L6-v2...zall-MiniLM-L6-v2T)show_progress_barz      Embedding shape: z2[1.2] Reducing dimensions with UMAP (384 -> 10)...
      r9   cosine*   )n_componentsn_neighborsmin_distmetricrandom_statez      Reduced shape: z [1.3] Clustering with HDBSCAN...r2      	euclideaneom)min_cluster_sizemin_samplesr0  cluster_selection_methodr1   z      Found z clusters, z noise points)r   topicsreddit_postssourcesregionssignal_typesr  r  r-   r  r  r  )r  r  r  r-   r  r  r9  r=  redditr:  r  r  i  r   )titler  r  r-   r  r  r;  r<  topic_countcross_source_countreddit_post_countc              3   J   K   | ]  }|j                  d       dk(  sd  yw)r  questionr1   Nrq   )rr   ts     r"   ru   z+layer1_embed_and_cluster.<locals>.<genexpr>  s      Z1quu]7Kz7YQZs   ##question_signal_counthas_question_signalr   c                     | d   S )Nr@   r   s    r"   r   z*layer1_embed_and_cluster.<locals>.<lambda>  s    a& r#   r   z-1zUnclustered/Noiselabel)total_topicstotal_clustersnoise_pointsclusters	timestampzanalysis_clusters.jsonwrH   indentclsz
[1.4] Saved: )"r   r#  r   r   rz   r   r   encodeshapeUMAPfit_transformHDBSCANfit_predictr   listcountr   ziprL   rJ   r   valuesry   sortedr   nowr   utc	isoformatr   r   r   dumpr   )r  r#  r   r   topic_textsrE  r   model
embeddingsreducerreduced	clusterercluster_labels
n_clustersn_noiserN  r   trendrJ  	label_str
topic_datacquestion_countsorted_clustersnoise_clusterr   output_pathr   s                               r"   layer1_embed_and_clusterrt  3  s    :	/	
+,	(O K !Z=C!J----D4 ! 
S-..N
OP 23EkTBJ	#J$4$4#5
67 

>?ii  G ##J/G	!'--
12 

,-!&	   I **73NS()"2FQANJ>"((,G	LKy
FG H&s6>'BC $<>E5J	H$# "55 ##HY  -HoHo7^< 99]I>

 	H%,,Z8N+//		-0ST ?h&Y/66|,";/ %n 5w7<Z7HE*-ds3bU|8  	I&**5?;I&**5?;I$<N __ 6AiL))AiL)) >!23.q{+-"%a	l"3
!$Q~%6!7
 Z(ZZ%3
!"#1A#5
 6 OO%?q<B)>?&O x &(l#!4g F$#\\(,,/99;F 77K	k3	 91		&!A<89	OK=
)*M3 	@*9 9s   P&PPP!clusters_datac                 
   ddl m} ddl}t        d       t        d       t        d        |t	        j
                  d            }g }g }g }| d	   }t        |dd
       D ]  \  }}	t        |	d   d d      dd }
dj                  |
D cg c]  }d|d    d|d    d c}      }d}|	j                  d      rct        |	d   d d      dd }g }|D ]3  }|j                  d|d    d|d    d|j                  dd       d       5 d d!j                  |      z   }d"d#j                  |	j                  d$g              }|	j                  d%      r|d&|	d'    d(z  }d#j                  |	d)         }d*| d+| d| d| d,	}	 |j                  j                  j                  d-d.|d/gd0d12      }|j                  d   j                  j                   j#                         }d}	 t%        j&                  |      }|d4dd5dd6}|j                  d7d4      j1                         }|d8vrd4}||d7<   i |	||j                  d9      d:}|j                  |       |j                  d;d      }d<}|d=k(  r|d>k\  rd}n|d?k(  r|d@k\  rd}|j                  d9d      }|r	dA|dd
  dBnd}|rB|j                  |       t        dC|dDz    dEt3        d
t5        |             dF| d&| dG| 
       nV|j                  |       |j                  dHd      ddI }t        dC|dDz    dEt3        d
t5        |             dJ| d&| dK| 
        i } |D ]<  }!|!j                  dNi       j                  d7dO      }| j                  |d      dDz   | |<   > t        dP       t        dQt5        |              t        dRt5        |              t        dSt5        |              t        dT|         t5        |      t5        |      t5        |      | ||t;        j<                  t>        j@                        jC                         dU}"tD        dVz  }#tG        |#dW      5 }$t%        jH                  |"|$dXtJ        Y       ddd       t        dZ|#        |"S c c}w # t$        j(                  $ ra |j+                  d3||j,                        }|r>	 t%        j&                  |j/                               }n# t$        j(                  $ r Y nw xY wY w xY w# t6        $ rS}t        dC|dDz    dLt9        |              d4ddMt9        |       dd6}|j                  i |	|dd:       Y d}~d}~ww xY w# 1 sw Y   xY w)[z
    Layer 2: Use Groq Llama 3.3 70B to classify and FILTER clusters.
    3-category system: EXPLAINER_OPPORTUNITY, CURIOSITY_MAGNET, KILL.
    r   )GroqNr$  z(LAYER 2: GROQ CLASSIFICATION + FILTERINGr%  r   )api_keyrN  2   r9  c                     | d   S )Nr-   rI  r   s    r"   r   z,layer2_classify_and_filter.<locals>.<lambda>  s
    7 r#   Tr   r2   
- r  z
 (source: r  rw   r   r:  c                 &    | j                  dd      S )Nr  r   rq   r   s    r"   r   z,layer2_classify_and_filter.<locals>.<lambda>  s    ~_`Ia r#   - r/r  : "r?  z" (r  z
 comments)zReddit: z | zSignal types: , r=  rG   (rF  z question signals)r;  zBClassify this topic cluster. Pick ONE category.

CLUSTER:
Topics: z

Sources: u  

CATEGORIES:

EXPLAINER_OPPORTUNITY = People want something EXPLAINED. Look for:
- Questions starting with "Why...", "How does...", "What is...", "ELI5..."
- Reddit posts asking for understanding, not just news
- Topics where the answer requires more than one sentence
- Examples that ARE explainer opportunities: "Why does every power plant boil water?", "How do undersea cables handle all internet traffic?", "What are the Epstein files?"
- Examples that are NOT: "Who won the Super Bowl?", "What time is the game?", "Breaking: CEO resigns"

CURIOSITY_MAGNET = Not a question, but fascinating. Look for:
- "What is this thing?" identification posts with high engagement
- Counterintuitive facts people love sharing
- "I never knew that" moments
- Examples: mystery object identification, surprising animal behavior, bizarre historical facts

KILL = Everything else. This includes ALL of the following:
- Sports scores, game results, player stats, team matchups
- Stock prices, market moves, financial tickers
- News headlines about events (elections, arrests, speeches, disasters) with no deeper "why" angle
- Celebrity gossip, entertainment news, movie/TV releases
- YouTube video titles (these are someone else's content)
- Music releases, concert announcements
- Weather, schedules, product launch dates
- Political news that is about WHAT HAPPENED not WHY IT WORKS THAT WAY
- Any topic that will be irrelevant in 2 weeks
- Topics where a Google search gives you the complete answer in 3 seconds

Ask: "Could someone make a 10-minute video explaining this that people would still watch 6 months from now?"
If YES → EXPLAINER_OPPORTUNITY or CURIOSITY_MAGNET
If NO → KILL

Respond in this exact JSON format:
{
  "classification": "EXPLAINER_OPPORTUNITY" or "CURIOSITY_MAGNET" or "KILL",
  "score": number from 1 to 10,
  "reason": "one sentence why",
  "the_question": "the specific question people want answered, or null if KILL"
}zllama-3.3-70b-versatileuser)rolecontentr'  r@   )rd  messages
max_tokenstemperature\{.*\}KILLzCould not parse Groq response)classificationr-   reasonthe_questionr  )EXPLAINER_OPPORTUNITYCURIOSITY_MAGNETr  r  )groq_classificationr  r-   Fr  rv   r         → ...  [r1   r0   u   ] ✓ z/10)r  (   u   ] ✗ z/10) - z	] Error: zClassification failed: r  r   z
[2.1] FILTER RESULTS:z      Total evaluated: z      PASSED: z      KILLED: z      Kill breakdown: )total_evaluatedtotal_passedtotal_killedkill_breakdownpassed_clusterskilled_clustersrO  zanalysis_classified.jsonrP  rH   rQ  z[2.2] Saved: )&r   rw  rI   rz   r   r   r   r^  joinrJ   r   chatcompletionscreatechoicesmessager  rO   r   loadsJSONDecodeErrorrP   DOTALLrQ   upperminr   	ExceptionrL   r   r_  r   r`  ra  r   r   rb  r   )%ru  rw  rI   clientall_classifiedr  r  rN  r   clusterr9  rE  
topic_listreddit_contextreddit_itemsreddit_linesr   signal_contextr;  promptresponseresponse_textr  
json_matchcls_typeclassified_clusterr-   passes_filterr  	q_displayr  ekill_reasonsro  r   rs  r   s%                                        r"   layer2_classify_and_filterr    s/   
 	/	
45	(O"))N34FNOOZ(H". Om
7)/CTRSUTUVYYX^_ST"Q|_$5Z(}A N_`
 ;;~&!'."9?akopqsrstLL! u##d1[>*:$qzl$quuUcefOgNhhr$stu'%**\*BBN *$))GKKPR4S*T)UV;;,-7+B#C"DDVWWN))GI./	 
 

     '-^K	m{{..55/#)f=>	 6 H %,,Q/77??EEGM "N!%M!: %&,=$(	" &))*:FCIIKHTT!/7N+,""'5 . 2 2> B"
 !!"45 #&&w2E!M22uz $//EQJ $)--nbAL:F%Sb 12#6BI&&'9:AaC5#b#h-"8!9zE7RVW`Vabc&&'9:'++Hb9#2>AaC5#b#h-"8!9zE7RYZ`YabcKOmd L ?,b1556F	R+//:Q>V? 
#%	#C$7#8
9:	N3/0
12	N3/0
12	"<.
12 ~.O,O,&**\\(,,/99;F 99K	k3	 91		&!A<89	M+
'(MU  `^ '' YYy-K
)-J4D4D4F)G// j  	mC!uIc!fX./"(3CF8< $	N ""#kg#knfj#kll	mD9 9su   R 
A S<%R:ES<
U2S98#SS9S2/S91S22S95S<8S99S<<	UAUUU$classified_datamax_clustersc                    t        d       t        d       t        d       d}d}d}|rddlm}  ||      }|r|j                  }|j                  xs d}	|j
                  xs d}
|j                  xs d}|j                  xs d}d|j                   d	|j                   d
}|
r	|d|
 d
z  }|r	|d| d
z  }|	r	|d|	 d
z  }|dz  }t        d|j                          | j                  dg       }d}|rY	 t        |t        | j                  dg             t        | j                  dg             t        |            }t        d|        |s@t        d       dg t        j                  t        j                         j#                         dS t        |      }|dkD  r&t        |      |kD  r|d| }t        d| d| d       nt        dt        |       d       g }t%        |      D ]  \  }}|j                  di       }|j                  d      xs( |j                  d      xs |j                  d      xs d }t        d!|d"z    d#t'        |      dd$  d%       |d&   D cg c]  }|d'   	 }}d
j)                  |D cg c]  }d(| 	 c}      }d)}|j                  d*      rg }|d*   D ]  }|j                  d+d      }|j                  d,d      }|j                  d-d.      }|j                  d/d      }|j                  d0d      dd1 }|j+                  d2| d3| d4| d5| d6	       |s|j+                  d7| d%        d
j)                  |      }d8j)                  |d9         } |d:   }!|j                  d;d      }"|j                  d<d       }#|j                  d,d      }$d}%|rd=| d
}%d>| d
|% d?| d@| dA|  dB|! dC|" dD}&	 t-        j.                  g dE|&dFdFdGH      }'|'j0                  dk7  rt        dI|'j2                  ddJ         )|'j4                  j7                         }(	 ddl})|)j;                  dK|(|)j<                        }*|*r$t?        j@                  |*jC                               }+nt?        j@                  |(      }+|dL   |+dL<   |dM   |+dM<   |d9   |+d9<   |d:   |+d:<   |j                  d<      |+d<   |j                  d,      |+dN<   ||+d<   |r||+dO<   tE        |+      },|,|+dP<   dQ|+v rdR|+vr	|+dQ   |+dR<   n	dR|+vrd|+dR<   |+j                  dSdT      }-tG        |+      }.|.|-k7  r|.|+dS<   dF|+dU<   |-|+dV<   |j+                  |+       |+j                  dSd       }/|+j                  dRd       ddW }t        dX|/ dY|,dZd[| d%        |D 0cg c]	  }0dP|0v s|0 }1}0|1jM                  db dFc       |D 0cg c]	  }0dP|0vs|0 }2}0tO        |1      }1|1|2z   }3tQ        |      }4|stS        |      nddg i}5tU        |5|3|      }6|6jM                  de dFc       t        |5j                  ddg             }7t        |3      }8t        |6      }9|s|7dkD  rt        df|7 dg|8 dh|9 di       nt        dj|9 dk       |t        |      |9|6t        j                  t        j                         j#                         dl}'tW        |      }:|:rt        dm|:        tY        |4dn      5 };t?        jZ                  |'|;dot\        p       ddd       t        dq|4        |rD|6rB	 t_        |||6drs      }<t        dtt        |<       du| d6       ta        |t        |6      dvw       ||'d{<   |'S # t        $ r}t        d|        Y d}~(d}~ww xY wc c}w c c}w # t>        jH                  $ rK}t        d\       |j+                  |j                  d]d       |(dd^ t'        |      |dL   d_       Y d}~Yd}~ww xY w# t,        jJ                  $ r t        d`       Y t        $ r"}t        dat'        |              Y d}~Dd}~ww xY wc c}0w c c}0w # 1 sw Y   MxY w# t        $ r2}t        dx|        |rta        |dyt'        |      z       Y d}~2d}~ww xY w)|a  
    Layer 3: Use Claude Opus for deep analysis of each surviving cluster.

    Args:
        classified_data: Output from Layer 2
        channel_id: Optional channel ID for channel-specific analysis context
        max_clusters: Limit analysis to N clusters (0 = unlimited, for testing use 3-5)
        replace_mode: If True, replace entire file. If False (default), merge with existing.
    r$  z"LAYER 3: CLAUDE OPUS DEEP ANALYSISr%  r   Nr   get_channelzK
CHANNEL CONTEXT:
You are analyzing opportunities for the YouTube channel "z".
Channel Identity: r{  z,The Itch (what viewer need this scratches): zFormat Pattern: z2Competitive Gap (what makes this channel unique): a  
IMPORTANT: Score topics higher if they directly exploit this channel's competitive gap. Score topics lower if they don't fit the channel identity, even if they're otherwise good opportunities. A great topic for another channel is a bad topic for THIS channel.
z[3.0] Channel context loaded: r  
all_trendsrN  )r   items_analyzedclusters_formedlayer2_passedz[3.0] Analysis run created: ID z-[WARN] Failed to create analysis run record: z)[WARN] No clusters passed Layer 2 filter!)total_analyzedr}   rO  z[3.0] LIMITED: Analyzing r0   z clusters (--max-clusters flag)z[3.0] Analyzing z clusters that passed filter...r  r  r  Unknownz
[3.r1   z] Analyzing: <   r  r9  r  r|  z No Reddit posts in this cluster.r:  r  r-   r  r  r?  r  r'  r~  r  z
" (score: z, comments: rw   z  Context: r  r;  rA  rF  r  u  
═══════════════════════════════════════════════════════════
CHANNEL-SPECIFIC HOOK CRITERIA
═══════════════════════════════════════════════════════════

u   You are a short-form video opportunity scout. Your job is to find the ONE specific detail in this cluster that would stop someone mid-scroll — then build a complete video concept around it.

u  

═══════════════════════════════════════════════════════════
RAW MATERIAL
═══════════════════════════════════════════════════════════

CLUSTER POSTS:
z

REDDIT SIGNALS:
z

SOURCES: z
CROSS-SOURCE COUNT: z
QUESTION SIGNALS: u+  

═══════════════════════════════════════════════════════════
PHASE 1: IDENTIFY THE HOOK MECHANISM
═══════════════════════════════════════════════════════════

Every viral hook uses one of these 5 mechanisms. Identify which ONE is strongest in this cluster:

1. CURIOSITY_GAP — "Wait, what?" A specific detail that contradicts what the viewer assumes to be true. They cannot scroll past without resolving it.
   Example: "CAPTCHAs Don't Actually Care About Your Answers"

2. SELF_RECOGNITION — "That's so me." Viewer sees their own behavior named in the first 1.5 seconds. Creates belonging.
   Example: "You're Not a Night Owl. You're Afraid of Tomorrow."

3. THREAT_INDIGNATION — "They're doing WHAT to me?" Reveals a specific manipulation the viewer experiences daily. Creates righteous awareness.
   Example: "Why the 'X' Button on Ads is Designed to Be Impossible to Tap"

4. CONSEQUENCE_CHAIN — "Then what happens?" A simple starting point with cascading non-obvious consequences. Creates domino-effect curiosity.
   Example: "What Actually Happens If You Never Pay a Parking Ticket"

5. NARRATIVE_SURPRISE — "That actually happened?" A real event that sounds like fiction. Lead with the most unbelievable detail.
   Example: "The Pope Who Put a Corpse on Trial"

Search this cluster for up to 3 candidate hooks. For each one:
- Quote the exact source post title
- Identify the hook mechanism
- Articulate the specific "wait, what?" moment in one sentence
- Rate scroll-stop strength 1-5 (5 = physically cannot scroll past this)

If NO hook with scroll-stop strength 4+ exists in this cluster, say so honestly. Not every cluster contains a video.

═══════════════════════════════════════════════════════════
PHASE 2: BUILD THE CONCEPT
═══════════════════════════════════════════════════════════

Take your strongest hook and build a video concept. The concept is about that ONE specific detail, not the general theme.

A. PREMISE (the video title)
The viewer must encounter something they cannot explain and need to resolve.

Rules:
- Must contain the specific detail that creates the information gap
- Must make someone with ZERO prior interest in this topic stop scrolling
- Under 15 words. The hook is in specificity, not length.
- NO listicle formats ("7 Ways...", "10 Things...")
- NO "Why You..." followed by something that makes viewer feel broken
- The viewer should feel CURIOUS, not lectured

CRITICAL FORMAT RULE: Do NOT default to the "You're Not X. You're Y." template.
This format has been overused across short-form content and triggers pattern
fatigue in algorithmic feeds. The orienting response fires on NOVELTY — a
premise that follows a predictable syntactic pattern fails at Phase 1 regardless
of how good the insight is.

Each premise must have a distinct sentence structure. If you find yourself
starting with "You're Not..." — stop and rewrite. The same insight can always
be expressed differently:

Instead of: "You're Not Lazy. Your Brain Is Missing a Start Button."
Try: "Your Brain Has No Start Button. That's Why You're Stuck."

Instead of: "You're Not a Night Owl. You're Afraid of Tomorrow."
Try: "The Real Reason You Stay Up Late Has Nothing to Do With Sleep."

The insight is the same. The format is fresh.

B. FIRST_FRAME
What text/image appears in the first frame to maximize scroll-stop? This is the thumbnail + opening visual moment.

C. TRIGGER_MAP
How this concept triggers the hook mechanism:
- Which of the 5 mechanisms does this use?
- What specific word/phrase/image creates the trigger?
- Why can't the viewer scroll past this?

D. OPENING HOOK (first 10 seconds)
The spoken words that OPEN the curiosity loop wider. Do NOT explain here — intensify the question.

E. CORE REVEAL
The satisfying explanation that closes the loop. Must be:
- Genuinely surprising (not "it's complicated")
- Explainable in 60+ seconds (if shorter, it's a fun fact, not a video)
- Leave viewer feeling smarter, not anxious

F. DEPTH CHECK
Can this sustain a 90-120 second video? Must have:
- A reveal that takes at least 60 seconds to properly explain
- At least one additional layer of surprise beyond the initial hook
- Enough substance that viewer learns something real, not just a fun fact

═══════════════════════════════════════════════════════════
PHASE 3: SCORING & VERDICT
═══════════════════════════════════════════════════════════

SCORING (1-5 each, be brutally honest):

SCROLL_STOP_POWER (25%): Can this premise make someone physically stop scrolling?
- 5: Impossible to scroll past. "Wait, WHAT?"
- 4: Very strong. Most people would stop.
- 3: Interesting but skippable. DANGER ZONE — this is where mediocre content lives.
- 2: Mild interest. "Huh, cool I guess."
- 1: No hook. Educational but not compelling.

COMPLETION_PROBABILITY (25%): Will viewers watch to the end?
- 5: The reveal is so satisfying they'll watch twice
- 4: Strong narrative pull, clear payoff
- 3: Might lose people in the middle
- 2: Hook is better than the content
- 1: No reason to keep watching

SHARE_SAVE_POTENTIAL (15%): Will viewers share this or save for later?
- 5: "I need to text this to someone RIGHT NOW"
- 4: Will send to specific friend who would care
- 3: Might share if asked
- 2: Unlikely to share
- 1: No share impulse

DEMAND_SIGNAL (15%): Is there evidence people care about this?
- 5: High engagement on the specific detail (not just the theme)
- 4: Strong Reddit/cross-source signals
- 3: Moderate engagement
- 2: Weak signals
- 1: No evidence of demand

VISUAL_POTENTIAL (10%): Can this be shown, not just told?
- 5: Rich visual reveal (mechanism, cross-section, transformation)
- 4: Good visual support available
- 3: Talking head viable
- 2: Hard to visualize
- 1: Pure audio content

EVERGREEN_POTENTIAL (10%): Will this work in 6 months?
- 5: Permanent truth about how the world works
- 4: Long-lasting relevance
- 3: Moderate shelf life
- 2: Tied to current events
- 1: Expires quickly

WEIGHTED SCORE = (scroll_stop * 0.25) + (completion * 0.25) + (share_save * 0.15) + (demand * 0.15) + (visual * 0.10) + (evergreen * 0.10)

═══════════════════════════════════════════════════════════
MAKE_NOW GATES (ALL must pass for MAKE_NOW verdict)
═══════════════════════════════════════════════════════════

Before giving a MAKE_NOW verdict, answer these 6 yes/no questions:

1. SCROLL_STOP_TEST: Does the premise contain a specific detail that creates an unresolved tension in under 3 seconds? (Not a theme — a detail)

2. UNIVERSAL_ACCESS_TEST: Would someone with zero interest in this topic still click? (Transcends subject matter)

3. DEPTH_TEST: Does the reveal require 60+ seconds to properly explain? (Not a fun fact — a video)

4. SHARE_TEST: Would someone text this premise to a friend unprompted? (Social currency)

5. SATISFACTION_TEST: Does the reveal leave the viewer feeling smarter, empowered, or validated — NOT anxious, guilty, or lectured? (Positive emotional payoff)

6. CHANNEL_FIT_TEST: Does this directly exploit this channel's specific itch and competitive gap? (Not just "good content" — good content FOR THIS CHANNEL)

VERDICT RULES:
- MAKE_NOW: All 6 gates pass. Weighted score 4.0+. Expected rate: 10-15% of clusters.
- WORTH_MAKING: 4-5 gates pass. Weighted score 3.0-3.9. Expected rate: 30-40%.
- NEEDS_RESEARCH: Hook is promising but facts need verification. Expected rate: 15-25%.
- SKIP: Fewer than 4 gates pass OR no genuine hook exists. Expected rate: 25-35%.

AUTOMATIC SKIP — Emotional Framing Guard:
Assign SKIP verdict immediately if ANY of these apply:
- Premise frames the viewer's existing behavior as pathological or broken
- Viewer would feel WORSE about themselves after reading the title (not during — after)
- Title uses shame as the hook mechanism (e.g., "You're Not Intense. You're Empty.")
- Title suggests the viewer is fundamentally defective rather than curious

═══════════════════════════════════════════════════════════
OUTPUT FORMAT (JSON)
═══════════════════════════════════════════════════════════

Respond in JSON with these exact fields:
{
  "hook_candidates": [
    {
      "source_post_title": "exact Reddit post title",
      "hook_mechanism": "CURIOSITY_GAP|SELF_RECOGNITION|THREAT_INDIGNATION|CONSEQUENCE_CHAIN|NARRATIVE_SURPRISE",
      "the_detail": "the specific surprising detail in one sentence",
      "scroll_stop_strength": 1-5
    }
  ],
  "best_hook_index": 0,
  "honest_assessment": "Is this cluster genuinely compelling or just analytically viable?",

  "premise": "the video title built around the specific detail",
  "first_frame": "what text/image appears in first frame",
  "trigger_map": {
    "mechanism": "which of the 5 mechanisms",
    "trigger_phrase": "the specific word/phrase/image",
    "why_irresistible": "why viewer can't scroll past"
  },
  "opening_hook": "first 10 seconds spoken",
  "core_reveal": "the satisfying explanation",
  "depth_check": "can this sustain 90-120 seconds? why?",
  "emotional_payoff": "smarter|empowered|validated|relieved|amused",
  "target_audience": "specific moment when someone would watch this",

  "scores": {
    "scroll_stop_power": {"score": 0, "reasoning": "..."},
    "completion_probability": {"score": 0, "reasoning": "..."},
    "share_save_potential": {"score": 0, "reasoning": "..."},
    "demand_signal": {"score": 0, "reasoning": "..."},
    "visual_potential": {"score": 0, "reasoning": "..."},
    "evergreen_potential": {"score": 0, "reasoning": "..."}
  },
  "weighted_score": 0.0,

  "make_now_gates": {
    "scroll_stop_test": true|false,
    "universal_access_test": true|false,
    "depth_test": true|false,
    "share_test": true|false,
    "satisfaction_test": true|false,
    "channel_fit_test": true|false,
    "gates_passed": 0
  },

  "verdict": "MAKE_NOW|WORTH_MAKING|NEEDS_RESEARCH|SKIP",
  "verdict_reasoning": "one sentence on why",
  "source_post_title": "the specific post that inspired this concept",
  "suggested_title": "same as premise (backward compat)",
  "structure": "3-4 section outline for the video"
})r   r   z--modelopusr   -Ti,  r   z    [ERROR] Claude CLI failed: d   r  r   r@  
groq_scorechannelr,   premisesuggested_titlere   rf   verdict_validatedr   ry  u       → z	 (score: .1fz) - z(    [WARN] Could not parse JSON responsethemei  )r  raw_responseparse_errorr   z     [ERROR] Claude CLI timed outz    [ERROR] c                     t        |       S r   r   r   s    r"   r   z&layer3_deep_analysis.<locals>.<lambda>A  s    #9!#< r#   r   r}   c                     t        |       S r   r   r   s    r"   r   z&layer3_deep_analysis.<locals>.<lambda>S  s    ,B1,E r#   z
[3.X] Merge: z existing + u    new/updated → z totalz
[3.X] Generated z opportunities)r   r  total_opportunitiesr}   rO  z[3.X] Backup: rP  rH   rQ  z[3.X] Saved: v3)r   analysis_run_idr}   prompt_versionz[3.X] DB: Inserted z opportunities (run_id: 	completed)r  opportunities_foundstatusz[WARN] DB insert failed: failed)r  r  error_messager  )1rz   channel_configr  namecompetitive_gapitch_descriptionformat_descriptionhook_criteriadescriptionrJ   r   r   r  r   r_  r   r`  ra  r   rL   r  r   r   r   r   r   stdoutrO   rI   rP   r  r   r  rQ   rc   r|   r  r   r   r   r   r   r   r   r   rb  r   r   r   )=r  r   r  r   channel_contextchannel_namer  r  r  r  r  r  r  r  r  r  r}   r   r  groq_clsr  rE  
all_titlestitles_textreddit_textr  r   commentsr-   r  r?  r  r;  rA  rF  r  r  hook_criteria_sectionr  r   r  rI   r  r)   weightedr   validated_verdictre   r   scored_oppsunscored_oppsr   rs  r   merged_opportunitiesexisting_count	new_countfinal_countr   r   opp_idss=                                                                r"   layer3_deep_analysisr    s	    
/	
./	(O OLM.j)"<<L%55;O&77=2!(!;!;!Ar#117RM#::A,, H&&' (O
  %QRbQcce#ff!%56H5I#LL%WXgWhhj#kk   O 27<<.AB &))*;R@O O		G1%"?#6#6|R#HI #O$7$7
B$G H!/2	O 3O3DEF 9:!hll3==?
 	
 'LaC0<?)-<8),qFefg _!5 66UVWM0 g+
7;;4b9{{>2yhll>6RyV^VbVbckVlypyacU-L(9#2(>'?sCD 07x/@A!aoA
Aii: >a2aS >? 9;;~&L^, E553gq)EE+y9	gr*55R0#6##d9+T%E7R^_g^hhi$jk ''+hZs(CDE ))L1K))GI./$%9: ',CQ G&ll+;YG\\'1-
 !#)
  %!        

 '( )() b*#sjH	+^^C#F   A%7ds8K7LMN"MM//1M2YYy-K
#zz**:*:*<=H#zz-8H *1)>&*1-*@'&-i&8#189M1N-.2:,,?O2P./)1g)>&+7(*4HY' 2(;-5)* (->h-N2:92EH./&h624H./ $,<<	6#B $4X$>!$(88*;HY'48H013CH/0$$X.",,y)< %6	B3BG	8C.UG3OPs
g+T ,E/?1/D1EKE<dK !.K11A1JQKMK *+6K#m3 )4KCO/
;VegiUjM /}>OQ]^ "EtT **?B?@N%&I*+KNQ./|I;FWXcWddjkl";-~>? !o.*-\\(,,/99;F ,J7K{m,- 
k3	 91		&!A<89	M+
'( /	0% /2#	G 'G~5MoM^^_`a  &$'(<$=" !0FM[  	GA!EFF	G8 B >f
 '' @B$$%\\'9=$1%4$8#&q6"),"7	&   (( 	746 	+LQ)**	+ F LR9 9*  	-aS12#*#"%a&	s   8A] )^ ^
A_+_+1E^
	`9`99	`>`>aAa 	]=$]88]=
_(A _#_+#_((_++`6`6`11`6a	b'bbzhttp://127.0.0.1:8100   i  c                  |    ddl } 	 | j                  t         dd      }|j                  dk(  S # t        $ r Y yw xY w)z)Check if the research service is running.r   Nz/healthr2   r   r'  F)requestsrJ   RESEARCH_SERVICE_URLstatus_coder  )r  r  s     r"   check_research_servicer    sH    <<#7"8 @!<L##s** s   (/ 	;;opportunitychannel_description
max_lengthc           	      j   t        | j                  d| j                  dd                  dd }t        | j                  dd            dd }t        | j                  d	| j                  d
d                  dd }|}|r|dvr	|d| z  }n|r|dvr|d| z  }t        |      |kD  r|d|dz
   dz   }|S )z
    Construct a web search query from Layer 3 output.

    Goes to Tavily (web search), so needs actual search terms,
    not instructions or creative titles.
    r  r  r  NP   core_revealr   x   the_itchopening_hookr  )r   N/ANoner&  r2  r  )rL   rJ   r   )r  r  r   r  revealitchr  s          r"   construct_research_queryr    s     +//)[__=NPY-Z[\]`^`aG34Tc:F{z;??>2+NOPQURUVDE& 331VH	$111TF
5zJozA~&.Lr#   r   thesisc                 N   ddl }	 d| i}|r||d<   |j                  t         d|d      }|j                  dk(  r|j	                         j                  d	      S t        d
|j                   d|j                  dd         y# t        $ r}t        d|        Y d}~yd}~ww xY w)z@Submit an async research job. Returns job_id or None on failure.r   Nr   r  z/research/asyncr  )r   r   r'  job_idz&    [ERROR] Research service returned : r  z+    [ERROR] Failed to submit research job: )	r  postr  r  r   rJ   rz   r   r  )r   r  r  payloadr  r  s         r"   submit_research_jobr    s    E" &GH==#$O4 ! 

 3&==?&&x00:8;O;O:PPRS[S`S`aebeSfRghi ;A3?@s   AB (B 	B$BB$r  c                 P   ddl }ddl}|j                         }	 |j                         |z
  }|t        kD  rt        d|  dt         d       y	 |j	                  t
         d|  d      }|j                  d	k7  rt        d
|j                          y|j                         }|j	                  d      }|dk(  r|j	                  di       S |dk(  rt        d|j	                  dd              y|j                  t               # t        $ r}t        d|        Y d}~yd}~ww xY w)zLPoll for research job completion. Returns result or None on failure/timeout.r   Nz    [TIMEOUT] Research job z
 exceeded sz
/research/r  r  r'  z    [ERROR] Poll failed: r  r  r   r  z"    [FAILED] Research job failed: errorzUnknown errorz    [ERROR] Poll error: )r  timeRESEARCH_MAX_POLL_TIMErz   rJ   r  r  r   sleepRESEARCH_POLL_INTERVALr  )	r  r  r  
start_timeelapsedr  datar  r  s	            r"   poll_research_jobr    s,   J
))+
*++/xzBXAYYZ[\	||';&<Jvh$OY[|\H##s*1(2F2F1GHI==?DXXh'F$xx"--8#:488G_;]:^_` JJ-.- 0  	,QC01	s+   AD 7D 
#D .D 	D%D  D%opportunities_datatop_nskip_researchc                 f
   t        d       t        d       t        d       | j                  d      }| j                  dg       }|st        d       | S d}|r4dd	lm}  ||      }|r$|j                  }t        d
|j
                          |rt        d       |D ]  }	d|	d<   d|	d<    | S t               s;t        dt        z          t        d       t        d       |D ]  }	d|	d<   d|	d<    | S t        dt                t        d| dt        |       d       t        |d d      }
|
d| }|
|d }|D ]%  }	d|	d<   d|	d<   d|	d<   d|	d<   d|	d<   d|	d <   d|	d!<   ' ddl
}d"}d}t        dt        |      |      D ]  }||||z    }||z  d#z   }t        |      |z   d#z
  |z  }t        d$| d%| d&t        |       d'       g }|D ]  }	|	j                  d(d)      dd* }t        d+| d,       t        |	|      }t        |	j                  d-|	j                  d(d                  }t        ||.      }|r#|j                  |	||f       t        d/|        d|	d<   d0|	d<    |D ]  \  }	}}|	j                  d(d)      dd* }t        d1| d2| d3       t!        |      }|r^|j                  d4      rLd|	d<   |j                  d5d      |	d<   |j                  d6d      |	d<   |j                  d7d      |	d<   |j                  d8d      |	d<   |j                  d9d      |	d <   |j                  d:d      |	d!<   ||	d;<   |d#z  }t        d<|j                  d7d       d=|j                  d8d       d>       |s|	j                  d?      %	 t#        ||	d?   d@|	j                  d      |	j                  d      |	j                  d      |	j                  d       |	j                  d;      |	j                  d!      dAB       d|	d<   dD|	d<   t        dE       |s|	j                  d?      	 t#        ||	d?   dFdGdDiB         ||z   | d<   t        |      ||t'        j(                  t*        j,                        j/                         dH| dI<   t        dJ| d%| dK       t1        |      }t3        |dL      5 }t5        j6                  | |d"t8        M       ddd       t        dN|        | S # t$        $ r}t        dC|        Y d}~d}~ww xY w# t$        $ r Y w xY w# 1 sw Y   NxY w)Oa  
    Layer 3.5: Deep Research Integration.

    Automatically triggers deep research for the top N opportunities using
    the Hybrid Research Service API.

    Args:
        opportunities_data: Output from Layer 3
        channel_id: Channel ID for context
        top_n: Number of top opportunities to research (default 5)
        skip_research: If True, skip research and just mark all as not researched

    Returns:
        Updated opportunities_data with research fields added
    r$  z$LAYER 3.5: DEEP RESEARCH INTEGRATIONr%  r  r}   z#[WARN] No opportunities to researchr   r   r  z[3.5.0] Channel: z0[3.5.0] Skipping research (--skip-research flag)Fr   Tresearch_skippedz)[WARN] Research service not available at zb       Run: cd /home/sietch6/research-sandbox/option1-gpt-researcher && python research_service.pyz=       Marking all opportunities as research_completed: falsezResearch service not availabler   z&[3.5.1] Research service available at z[3.5.2] Will research top z opportunities (of z total)c                 &    | j                  dd      S )Nr,   r   rq   r   s    r"   r   z'layer35_deep_research.<locals>.<lambda>9  s    aee,a0 r#   r   Nr   r   r   r   r   r   rH   r1   z
[3.5.3] Processing batch r0   r  z topics)r  r  ry  z  Submitting: r  r  r  u       → Job ID: zFailed to submit research jobz  Polling: z (job z)...successreport_pathreport_content
word_countsource_countduration_secondsgenerated_atr   u       ✓ Completed: z words, z sourcesr   r  )r'  r(  r)  r*  r  r+  )r  r   research_statusresearch_dataz*    [WARN] DB update for research failed: z Research job failed or timed outu       ✗ Failedr  r  )r  researched_counttop_n_requestedresearch_timestampresearch_statsz
[3.5.4] Research complete: z successfulrP  rQ  z[3.5.5] Saved: )rz   rJ   r  r  r  r  r  r  r   r^  r  ranger  rL   r  r   r  r   r  r   r_  r   r`  ra  r   r   r   rb  r   )r  r   r  r   r  r}   r  r  r  r   r  to_researchnot_researchedr  
batch_sizer.  batch_startbatch	batch_numtotal_batchesjobsr?  r  r  r  r   r  rs  r   s                                r"   layer35_deep_researchr;    s/   * 
/	
01	(O ),,->?O&**?B?M34!! .j)")"5"5%gll^45@A  	+C(-C$%&*C"#	+ "! "#9<PPQrsMN  	EC(-C$%$DC !	E "!	23G2H
IJ	&ug-@]AS@TT[
\] 0K fu%K (N  ,$) !&*"#)-%&%)!"'+#$+/'('+#$, JQK 0*= MKj(@A J.!3	[)J6:zI+I;abUT\]^  	HCGG-y9#2>EN5'-.,S2EFECGG4Er,JKLF(v>FS&%01(12,1()(G$%	H  #' 4	CGG-y9#2>EKwfVHD9:&v.F&**Y/,0().4jj.K*+17<Lb1Q-.-3ZZa-H)*/5zz.!/L+,39::>PRS3T/0/5zz."/M+,(-$% A% +FJJ|Q,G+HQWQ[Q[\jlmQnPoowxy #sww|'<'HP>,;'*<'8,725'':S2T.1gg6K.L038O0P47GG<W4X),1A)B038O0P+	  -2()(J$%( #sww|'<'H>,;'*<'8,4+24V*W	[4	3M` +6*F'"=1, &ll8<<8BBD	,'( 
)*:);1UG;
OP )4K	k3	 E1		$a|DE	OK=
)*I % P J1#NOOP  %  E Es7   A3S2/T<T'2	T;TT	T$#T$'T0opportunity_idc           	      |   t        |      }|j                         st        d|        yt        |d      5 }t	        j
                  |      }ddd       j                  dg       }d}d}t        |      D ]I  \  }}	t        |	j                  d|            t        |       k(  st        |      t        |       k(  sE|	}|} n |t        d|  d       y|j                  d	      rt        d
       yd}
|rddl	m
}  ||      }|r|j                  }
t               st        d       y|j                  dd      }t        d|        t        ||
      }t        |j                  d|j                  dd                  }t        ||      }|syt        d|        t        |      }|r|j                  d      rd|d	<   |j                  dd      |d<   |j                  dd      |d<   |j                  dd      |d<   |j                  dd      |d<   |j                  dd      |d <   |j                  d!d      |d"<   ||d#<   |||<   ||d<   t        |d$      5 }t	        j                   ||d%&       ddd       t        d'|j                  dd       d(       yt        d)       y# 1 sw Y   ZxY w# 1 sw Y   BxY w)*a  
    Manually trigger research for a specific opportunity.

    Args:
        opportunity_id: The ID of the opportunity to research
        channel_id: Optional channel ID for context

    Returns:
        True if research was successful, False otherwise
    z'[ERROR] No opportunities file found at Fr   Nr}   idz[ERROR] Opportunity z
 not foundr   z1[INFO] Opportunity already has research completedTr   r   r  z&[ERROR] Research service not availabler  r  zResearching: r  r$  zJob submitted: r%  r&  r   r'  r   r(  r   r)  r   r*  r   r+  r   r   rP  rH   )rR  u   ✓ Research completed: z wordsu   ✗ Research failed)r   r   rz   r   r   r   rJ   r   rL   r  r  r  r  r  r  r  rb  )r<  r   rs  r   r  r}   
target_opp
target_idxidxr   r  r  r  r?  r  r  r  r   s                     r"   run_research_for_opportunityrB    s    )4K7}EF	k3	 1yy| HH_b1M JJm, SswwtS!"c.&99SX^I\=\JJ	 $^$4J?@~~*+AC .j)")"5"5 "#68 NN,i8E	M%
!"$Z1DEE	:>>:KR+PQRF v6F	OF8
$%v&F&**Y'+/
'(-3ZZr-J
)*06

;KR0P
,-,2JJ|Q,G
().4jj.K
*+28**=OQR2S
./.4jj.L
*+',
#$ %/j! -_+s# 	)qIIdAa(	) 	(L!)D(EVLM#%M ~	) 	)s   J%J2%J/2J;max_lenc                    | yt        | t              r*dD ]  }|| v st        | |   |      c S  t        |       d| S t        | t              r| rt        | d   t              rtg }| dd D ]T  }|j                  d|j                  d|j                  dd	                  }|s8|j                  t        |      dd
        V |rdj                  |      d| S dj                  d | dd D              d| S t        |       d| S )zFSafely convert a value to a truncated string, handling dicts and None.Nr  )r.   r   r  summaryr  formattyper      sectionr?  r  r      r  r  c              3   8   K   | ]  }t        |      d d   y w)N   )rL   )rr   vs     r"   ru   zsafe_str.<locals>.<genexpr>&  s     6Q6s   )r   rK   safe_strrL   rZ  rJ   r   r  )rV   rC  rU   sectionsrM  r?  s         r"   rN  rN    s   
{#t[ 	3CczC'22	3 3x!!#t:c!fd+H!W 5iwfb8I)JKOOCJsO45 ||H-hw77yy6c"1g66x@@s8HWr#   c           
      h   t        d       t        d       t        d       | j                  dg       }|st        d       y|D cg c]0  }dt        |j                  dd	            j                         v s/|2 }}|D cg c]0  }d
t        |j                  dd	            j                         v s/|2 }}|D cg c]0  }dt        |j                  dd	            j                         v s/|2 }}|D cg c]0  }dt        |j                  dd	            j                         v s/|2 }}t	        |      }t        d       t        dt	        |       dt	        |      |z  dz  dd       t        dt	        |       dt	        |      |z  dz  dd       t        dt	        |       dt	        |      |z  dz  dd       t        dt	        |       dt	        |      |z  dz  dd       |rt        d       t        d       t        d       t        |dd       D ]X  \  }}	d|	v rt        d|dz    d       t        |	j                  d      d      }
t        |	j                  d       d!      }|	j                  d"d#      }t        |t              r"|j                  d$|j                  d%d#            }	 t        |      }t        |	j                  d'      d(      }t        |	j                  d)      d      }t        |	j                  d*      d+      }t        d|dz    d,|
 d-|d.d/       t        d0|        t        d1|        |r|d2k7  rt        d3| d4       |sD|d2k7  sKt        d5|        [ |rt        d       t        d6       t        d       t        |dd       D ]  \  }}	d|	v rt        |	j                  d       d!      }|	j                  d"d#      }t        |t              r"|j                  d$|j                  d%d#            }	 t        |      }t        d7|dz    d8|d.d9|         |rt        d       t        d:       t        d       t        |dd;       D ]  \  }}	t        |	j                  d       d      }t        |	j                  d<      d+      }|d2k(  rt        |	j                  d=      d+      }|	j                  d"d#      }t        |t              r"|j                  d$|j                  d%d#            }	 t        |      }t        d7|dz    d8|d.d9|        t        d>|         |rt        d       t        d       t        d       t        |dd;       D ]]  \  }}	t        |	j                  d       d      }t        |	j                  d=      d+      }t        d7|dz    d,|        t        d?|        _ yyc c}w c c}w c c}w c c}w # t        t        f$ r d&}Y :w xY w# t        t        f$ r d&}Y w xY w# t        t        f$ r d&}Y w xY w)@z8Print a human-readable summary of deep analysis results.r$  zDEEP ANALYSIS SUMMARYr%  r}   zNo opportunities analyzed.Nrg   re   r   rx   r   rf   z
VERDICT BREAKDOWN (4 tiers):z  MAKE_NOW:       r  r  z.0fz%)z  WORTH_MAKING:   z  NEEDS_RESEARCH: z  SKIP:           z=
------------------------------------------------------------z MAKE NOW (immediate opportunity)z<------------------------------------------------------------r2   r  z
[r1   z"] Parse error - raw response savedr  ry  r  F   r,   r   r-   r.   rG   suggested_formatr  r  	structurer  z] z	 (Score: r  z/5.0)z    Title: z    Format: r  z
    Hook: r  z    Structure: zWORTH MAKING (no rush)r  z] (z) zNEEDS RESEARCHr2  research_neededverdict_reasoningz       NEEDS: z       Why: )rz   rJ   rL   r  r   r   rN  r   rK   r   rM   	TypeError)r  r}   r   make_nowworth_makingneeds_researchskipr/   r   r   r  r?  r-   fmthookrS  rT  r  s                     r"   print_summaryr]  *  s   	/	
!"	(O&**?B?M*+ )\aJ#aeeIr>R:S:Y:Y:[,[\H\,d!#aeeIWYFZB[BaBaBc0cAdLd!.hA2Bc!%%PY[]J^F_FeFeFg2gahNh$T!#aeeIr6J2K2Q2Q2S(SATDTE	*,	s8}oRHe0CC0G/LB
OP	s<01C4Ee4KC4OPS3TTV
WX	s>232c.6I%6OPS6STW5XXZ
[\	s4ykCIeOC,?+DB
GHo01h!- 	5FAs$AaC5 BCDSWWW-r2ESWW%67<EGG,a0E%&		'599Wa+@Ae 377#56;CCGGN3S9D !5r:IC!uBugYuSk?@Kw'(L&'
4&,-Y%/	{343	56 o&'hRa 01 	6FAs$SWW%67<EGG,a0E%&		'599Wa+@Ae C!uCc{"UG45	6 ohr 23 	6FAsSWW%67<E&sww/@'A2FO%'"*3773F+G"LGG,a0E%&		'599Wa+@Ae C!uCc{"UG45N?"345	6 ofhRa) 	+FAsSWW%67<Ecgg&9:B?FC!uBug&'L)*		+	 c ]dhT4 	* : 	* $ 	* sf   0W7W0W2W<0W"-W"70W'(W';W,XX,X XXXX10X1c                     ddl } | j                  d      }|j                  ddd       |j                  d	t        d
       |j                  dt        d       |j                  dt        d       |j                  ddd       |j                  dt        dd       |j                  ddd       |j                  ddd       |j                  dt        dd       |j                  dt        dd       |j                  d dd!       |j                  d"t        d#d$       |j                  d%t        d&d'       |j                         }|j                  rdd(lm}m	} t        d)        |       j                         D ]P  \  }} ||      }t        d*| d+|j                          t        d,|d-           |d.   s@t        d/|d.           R t        j                  d       |j                  r[t        d0       t        d1       t        d0       t!        |j                  |j"                  2      }t        j                  |rdnd       |j$                  s|j&                  dd3lm}	m}
m}  |
|j"                        }|s.t        d4|j"                   d5       t        j                  d       t        d6|        t1        |d7      5 }t3        j4                  |      }ddd        |	|j"                  |j&                  |j6                  |j8                  8      } |||j"                        }t        d9       t        d:|        t        d;t;        |j"                                t        j                  d       t        d0       t        d<       |j"                  r`dd=lm}  ||j"                        }|rt        d>|j                          n-t        d?|j"                          t        j                  d       t        d0       t        d@t?        j@                  tB        jD                        jG                                 tI                tK                	 tM                d}|jP                  rtS               }t        dB| dC       n&|jT                  r|jT                  }t        dD|        t        dE       tW        |jX                  ||j"                  F      }t        dGt[        |       dH       |j"                  rt        dI|j"                   dC       i }|D ],  }|j]                  dJdK      }|j]                  |d      dz   ||<   . t        dL|        t[        |      dMk  r t        dN       t        j                  d       t_        |      }ta        |      }tc        ||j"                  |jd                  |jf                  O      }ti        ||j"                  |jj                  |j8                  P      }tm        |       t        dQ       	 dRdSg}|j"                  r|jo                  d|j"                  g       tq        jr                  |dTdTdUV      }|jt                  dk(  rt        dW       nt        dX|jv                          t        dZ       t        d[       t        d0       t        d\       t        d]tx         d^       t        d]tx         d_       t        d]t;        |j"                                t        d]tx         d`       t        datx         db       y# 1 sw Y   KxY w# tN        $ r}t        dA|        Y d}~d}~ww xY w# tN        $ r}t        dY|        Y d}~d}~ww xY w)cz"Run the 3-layer analysis pipeline.r   Nz3-Layer Trend Analysis Pipeline)r  z--since-last-run
store_truez6Only analyze items from the most recent collection run)actionhelpz--since-runz+Only analyze items from runs >= this run ID)rG  ra  z--limitz Maximum number of topics to loadz	--channelz)Only analyze items tagged to this channelz--list-channelsz List available channels and exitz--top-nr1   zFNumber of top opportunities to auto-research in Layer 3.5 (default: 1))rG  r   ra  z--skip-researchz!Skip Layer 3.5 deep research stepz	--replacezTReplace entire opportunities file instead of merging (destructive, use with caution)z--research-onlyOPPORTUNITY_IDzARun research for a specific opportunity ID only (skip Layers 1-3))rG  metavarra  z--max-clusterszILimit Layer 3 analysis to N clusters (0 = unlimited, for testing use 3-5)z--generate-videosuI   Run Layers 4a→4b→4c video generation for all researched opportunitiesz--generate-videoIDuE   Run Layers 4a→4b→4c for a specific opportunity by index (0-based)z--prompt-versionv1z1Prompt version for video generation (default: v1))get_enabled_channelsget_channel_statszAvailable channels:z  r  z    Items tagged: 
item_countlast_analysisz    Last analysis: r%  zMANUAL RESEARCH TRIGGERr   )&run_video_generation_for_opportunitiesget_latest_opportunities_filesave_opportunities_timestampedz1[ERROR] No opportunities file found for channel 'z'. Run analysis first.z[INFO] Loading from: r   )r  r   specific_idr  r   z 
[OK] Video generation complete.z     Saved: z     Latest: zTREND ANALYSIS PIPELINEr  z	Channel: z[ERROR] Unknown channel: z	Started: z[WARN] Migration check failed: z0
[MODE] Analyzing only from latest run (run_id: rw   z!
[MODE] Analyzing from run_id >= z$
[0] Loading trends from database...)r  r	  r   z    Loaded z unique topicsz    (filtered to channel: r  r  z    Signal types: r)  zF[ERROR] Not enough data to analyze. Run the collection pipeline first.)r   r  r   )r   r  r   z!
[4] Generating dashboard data...python3zgenerate_dashboard_data.pyTr  )r   r   r   z'    Dashboard data updated successfullyz*    Warning: Dashboard generation failed: z0    Warning: Could not generate dashboard data: r$  zANALYSIS COMPLETEzOutput files:z  - z/analysis_clusters.jsonz/analysis_classified.jsonz/dashboard_data.jsonz
Dashboard: file://z/dashboard.html)=argparseArgumentParseradd_argumentr   rL   
parse_argslist_channelsr  rf  rg  rz   rR   r  r   r   research_onlyrB  r  generate_videosgenerate_videovideo_generationrj  rk  rl  r   r   r   r  r   r   r  r   r_  r   r`  ra  r   r   r   r  since_last_runr  	since_runr!  r  r   rJ   rt  r  r  r  replacer;  r  r]  extendr   r   r   r   r   )ro  parserargsrf  rg  ch_idchstatsr%  rj  rk  rl  
input_pathr   r  
saved_pathr  r  r	  r  signal_countsrE  stru  r  cmdr   s                              r"   mainr    s2   $$1R$SF
E  
 :  
 /  
 8  
 /  
 U	   0  
 c  
  P	   X	   X  
 T	   @	   D J#$-/557 	FIE2%e,EBugRy)*&u\':&;<=_%+E/,B+CDE	F 	 h'(h.t/A/Adll[g1% t22>	
 	
 34<<@
Edll^SijkHHQK%j\23*c" 	.a!%1	. D1||++..,,
 44FU
13ZL)*4T\\BCDE	(O	
#$||.&IbggY'(-dll^<=HHQK	(O	Ihll8<<0::<=
>? 5
 L&(A,qQR	~~2<.AB 

12 jj!<<F
 
KF}N
34||*4<<.:; M 9UU=),)--b!4q8b9 
}o
./
6{RVW -V4M 1?O .<<&&\\	 /<<jj((	 $% 

./F67<<JJT\\23	
 !;<>v}}oNO 
/	
	(O	/	D3
45	D5
67	D'56
78	D0
12	 O
<=}	. 	.P  5/s3445V  F@DEEFs=   \1&
\> A9]# 1\;>	] ]] #	^,]??^__main__r   )F)NNN)Nr   F)i|  )Nr2   F)r  )?r'   r   r   r   r   r   r   r   pathlibr   typingr   r   r   r	   numpyr   dotenvr
   db_opportunitiesr   r   r   r   r   r   JSONEncoderr   rL   r   rc   r|   r   __file__parentr   r  r   r   r   boolr   r   r   r   r  r!  rt  r  r  r  r  r  r  r  r  r  r;  rB  rN  r]  r  r$   rI  r#   r"   <module>r     s    	   
 '  , ,   	$4## 	$@T#s(^ @ @F"tCH~ "# "J6#4: 6#$t* 6#t (^""X-

*
*6s 6d 6 # $ *;C ;4S> ;( LS>LDcN+L L 
$sCx.	L^*(8)# ) QQQ Q 
$sCx.	QpET$sCx.%9 Ed38n EX@d38n @c3h @NH$sCx. Hc H`c Hw{ H  IM  NQ  SV  NV  IW H^ /    $sCx. s `c nq 0s C 8C= .!c !htCH~&> !L 	xS#Xxx x 	x
 
#s(^xvW W# WQU W|3  4f+d38n f+Rn>b zF r#   