
    i|9              	      J   U d Z ddlmZ ddl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mZmZ  ej                  d      ZdZded<    eh d	      Zd
ed<   dZdZdZdZ ej0                  d       G d d             Z G d de      Zg dg dg dg dg dg ddZded<   g dg d g d!g d"g d#g d$g d%d&d'gd(Zded)<   d*Zded+<   d,Zd-ed.<   dCd/Z dDd0Z!d1d2d3d4d5Z"ded6<   dEd7Z#dFd8Z$dGd9Z%dHd:Z&dHd;Z'dFd<Z(dIdJd=Z)d>Z*ded?<   dKd@Z+dLdMdAZ,e-dBk(  r e. e,             y)Nu  IDS Phase 6 — natural language routing engine.

Rule-based intent classifier + size/style extractors + routing matrix.
Returns a deterministic `RoutingDecision` for natural-language prompts so
that callers (dispatch / chairman UI) can dispatch the correct IDS skill
without the engine itself reaching out to external LLM APIs.

Design notes (see memory/plans/tasks/task-2394/context-notes.md):
- Rule-based: no Haiku call. Sub-2s SLA + deterministic for regression tests.
- External API gate: explicit `block_direct_api_call(intent, caller)` — design-team only.
- Dual-version cardnews routing (ThreadAuto render vs. satori) decided by body length / source.
- Confidence-based confirmation: ≥0.85 auto, ≥0.55 confirm, <0.55 ambiguous fallback.
    )annotationsN)IterableOptionalSequenceTupleids_natural_routing)cardnewspptmobilemotionimagecode	ambiguouszTuple[str, ...]INTENTS>   r
   r   r   r   r	   	frozensetDESIGN_TEAM_INTENTSg333333?g?i  i`  T)frozenc                      e Zd ZU dZded<   ded<   ded<   ded<   ded<   d	ed
<   ded<   ded<   d	ed<   ded<   ded<   ded<   ddZy)RoutingDecisionz?Deterministic routing output. All fields are JSON-serializable.strintentskillOptional[str]fallback_skillstylesizefloat
confidenceboolneeds_confirmationconfirm_message
elapsed_mssla_okdict
diagnosticprompt_hashc                X    t        j                  t        j                  |       dd      S )NFT)ensure_ascii	sort_keys)jsondumpsdataclassesasdict)selfs    L/home/jay/workspace/.worktrees/task-2463-dev6/scripts/ids_natural_routing.pyto_jsonzRoutingDecision.to_jsonG   s!    zz+,,T2RVWW    N)returnr   )__name__
__module____qualname____doc____annotations__r0    r1   r/   r   r   6   sP    IKJ!!
""LXr1   r   c                      e Zd ZdZy)RoutingErrorzLRaised when an external API call is attempted from a non-design-team caller.N)r3   r4   r5   r6   r8   r1   r/   r:   r:   K   s    Vr1   r:   )
)   카드뉴스      @)u   카드 뉴스r<   )r	   r<   )z	card newsr<   )u   인포그래픽       @)infographicr=   )u   배너      ?)bannerr?   )	instagram333333?)	   인스타rB   ))r
   r<   )u   덱      @)u   발표rD   )u   발표자료r<   )u   슬라이드r=   )u   슬라이드 덱r<   )z
pitch deckr<   )u	   매거진r?   )u   프레젠테이션r<   )presentationrD   )u	   키노트r=   ))   모바일 프로토타입      @)zmobile prototyperG   )iphonerD   )u	   아이폰rD   )pixelrD   )u   픽셀r=   )u
   앱 시연r<   )u
   앱 화면rD   )u   프로토타입r?   )u   프로토 타입r?   )u   베타테스트 시연r<   ))   모션 카드뉴스rG   )u   동영상 카드뉴스rG   )mp4rD   )u	   동영상r=   )u	   리얼스r=   )reelsr=   )   쇼츠r=   )shortsr=   )u   애니메이션rD   )	animationr=   )videor=   )zhtml to videor<   ))   광고 이미지r<   )u   광고 사진r<   )u   광고 포토r<   )zad imager<   )zad photor<   )u   포토리얼rD   )	photorealrD   )u   사진 느낌r=   )u   실사r=   )u   배경 이미지r=   )u   이미지 만들      ?))reactr<   )u	   리액트r<   )znext.jsr<   )u   랜딩 페이지rD   )u   랜딩페이지rD   )zlanding pagerD   )u   웹 컴포넌트rD   )u   ui 컴포넌트r=   )u   프론트엔드r?   )frontendr?   )u   코드rS   )	componentr?   r	   r
   r   r   r   r   r$   INTENT_KEYWORDS)rC   rA   igu   인스타그램)u	   스토리storystories)u   페이스북u   페북facebookfb)z x u	   트위터twitteru   엑스)	   스레드threadsthread)	   네이버naveru	   블로그blog)rM   rN   u	   유튜브youtubeu   릴스rL   )instagram_squareinstagram_storyr\   	x_twitterr`   
naver_blogyoutube_shortsrL   SIZE_KEYWORDS)supabasestripelinearnotionvercelfigmaappleu   토스u   토스뱅크u	   카카오rb   u   라인STYLE_BRANDS))u	   미니멀minimal)u	   럭셔리luxury)u	   뉴트로retrovintage)u   브루탈리즘	brutalist	brutalism)u   뉴모피즘neumorphism)u   글래스모피즘glassglassmorphism)u   다크dark)u	   라이트lightzTuple[Tuple[str, ...], ...]STYLE_TONESc                B     |dkD  xs t         fddD              }|ryy)u(  Dual-version cardnews routing (memory feedback: alternate satori / threadauto).

    Rule:
      - Long body (> 80 chars) OR explicit "스레드" keyword → threadauto_render (primary), satori-cardnews (fallback)
      - Otherwise → satori-cardnews (primary), threadauto_render (fallback)
    P   c              3  &   K   | ]  }|v  
 y wNr8   .0kprompts     r/   	<genexpr>z"_route_cardnews.<locals>.<genexpr>   s     +YAAK+Y   )r_   ra   )threadauto_rendersatori-cardnews)r   r   any)r   
body_charslong_or_threads   `  r/   _route_cardnewsr      s(      "_Y+YAX+Y(YN51r1   c                4     t         fddD              }|ryy)u   Ad photo: gemini-image primary, hybrid-image fallback for heavy text.

    GPT image (Codex path) is referenced as 2순위 in task md but not used at this layer.
    c              3  &   K   | ]  }|v  
 y wr   r8   r   s     r/   r   z_route_image.<locals>.<genexpr>   s     dfdr   )u	   텍스트u   한글 텍스트u   카피u   문구)hybrid-imagegemini-image)r   r   r   )r   has_heavy_texts   ` r/   _route_imager      s     d.cddN-)r1   )zmagazine-ppt-koN)zmobile-prototype-koN)zmotion-cardnews-koN)zfrontend-designN)r
   r   r   r   _FIXED_ROUTESc                D    | t         v r|dk7  rt        d|  d|d      yy)u   Enforce IDS §0.5: design-team intents must not be invoked by external API callers.

    Raises `RoutingError` when a non-design-team caller targets a design-team intent.
    design_teamzintent=z) requires caller=design_team (got caller=u8   ); 외부 API 직접 호출 차단 게이트 (IDS §0.5)N)r   r:   )r   callers     r/   block_direct_api_callr      sE     $$=)@fXFvj QD D
 	
 *A$r1   c                >    | j                         j                         S r   striplowerr   s    r/   
_normalizer     s    <<>!!r1   c                   t         D ci c]  }|dk7  s	|d }}t        j                         D ]!  \  }}|D ]  \  }}|| v s||xx   |z  cc<    # t        |j                         d d      }|d   \  }}t	        |      dkD  r|d   d   nd}	|dk(  rdd|fS ||	z
  }
t        d||d	z   z  d
d|
t        |d      z  z  z   z        }|t        k  r
|
dk  rd||fS |||fS c c}w )zHScore every intent against the prompt and return the winner + score map.r   g        c                    | d   S )N   r8   )xs    r/   <lambda>z_score_intent.<locals>.<lambda>  s
    !A$ r1   T)keyreverser   r   gGz?r=   rB   g?rS   g      ?)r   rX   itemssortedlenminmaxCONFIRM_CONFIDENCE)textr   scoreskwskwweightranked
top_intent	top_scorerunner_up_scoremarginr   s               r/   _score_intentr     s%    /6OF;9NFCKOFO&,,. ) 	)JBTzv&(	)) FLLNEF"1IJ	&)&kAofQil3O A~C''(FTIS9cC6TWXacfTgKgDh>hijJ&&6C<J..z6))% Ps
   
CCc                n     t         j                         D ]  \  }}t         fd|D              s|c S  y )Nc              3  ^   K   | ]$  }|j                         j                         v  & y wr   r   )r   r   r   s     r/   r   z _extract_size.<locals>.<genexpr>!  s%     8brxxz!T)8s   *-)rk   r   r   )r   
size_labelr   s   `  r/   _extract_sizer     s8    (..0 
C8C88 r1   c                h    t         D ]
  }|| v s|c S  t        D ]  }|D ]  }|| v s|d   c c S   y )Nr   )rs   r   )r   brandtonestones       r/   _extract_styler   &  sR     D=L    	 Dt|Qx	   r1   c                l    t        j                  | j                  d            j                         d d S )Nzutf-8   )hashlibsha256encode	hexdigestr   s    r/   _hash_promptr   1  s*    >>&--01;;=crBBr1   c                   | r| j                         st        d      t        j                         }t	        |       }t        |      \  }}}t        |      }t        |      }t        |       }	|dk(  rt        ||	      \  }
}n.|dk(  rt        |      \  }
}n|t        v rt        |   \  }
}nd\  }
}|t        v rt        ||       |dk(  xs	 |t        k  }d}|r2|dk(  rd}n*dd	d
ddddj                  ||      }|rd| dnd}| | d}t        j                         |z
  dz  }|t         k  }ddd||	d}t#        ||
|||t%        |d      ||t%        |d      ||t'        |             }t(        j+                  d|j,                  |j.                  |j0                  |j2                  |j4                  |j6                         |S )a  Route a natural-language prompt to the matching IDS skill.

    Args:
        prompt: User natural-language input (Korean/English).
        caller: Caller identity. Must be ``design_team`` for design intents (gate).

    Returns:
        RoutingDecision with intent, skill, style/size, confidence, SLA telemetry.

    Raises:
        RoutingError: when a non-design-team caller targets a design-team intent.
    zprompt must be non-emptyr	   r   )zids-router::clarifyNr   Nu`   어떤 산출물을 원하시나요? (카드뉴스/PPT/모바일/모션/광고 이미지/코드)r;   u   PPT/덱rF   rJ   rQ   u   프론트엔드 코드rW   z (u    스타일) u   로 진행할까요? (Y/n)g     @@routerule_based_no_modelnone)phasemodel_qualitycli_constraintr   r      )r   r   r   r   r   r   r    r!   r"   r#   r%   r&   zHids_route hash=%s intent=%s skill=%s conf=%.2f confirm=%s elapsed=%.2fms)r   r:   timeperf_counterr   r   r   r   r   r   r   r   r   r   AUTO_CONFIDENCEgetSLA_ROUTE_MSr   roundr   LOGinfor&   r   r   r   r    r"   )r   r   startedr   r   r   	score_mapr   r   r   r   fallbackr    r!   label
style_partr"   r#   r%   decisions                       r/   r   r   8  s    566!GfD$1$$7!FJ	D4 EVJ)$
;x	7	&t,x	=	 '/x5x $$ff- K/RZ/5Q%)O[  AO + 5/+0 c&&!  5:2eWK0rJ!&
|3MNO##%/69J<'F.  J Q'-'Q' (H HHR## Or1   )u"   supabase 스타일 보험 PPT 5장u)   인스타그램 카드뉴스 만들어줘u.   iPhone 15 Pro 모바일 프로토타입 시연u0   광고 포토 이미지 — 한글 카피 가득u.   토스 스타일 랜딩 페이지 React 코드_DEFAULT_DEMO_PROMPTSc                   d| j                    d| j                   | j                  rd| j                  z   nd d| j                   d| j                   d| j
                  d| j                  rd	nd d
| j                  dd| j                   dS )N[z] skill=z / r   z	 | style=z size=z | conf=z.2fz
 [confirm]z | zms (sla_ok=))	r   r   r   r   r   r   r    r"   r#   )r   s    r/   _format_decisionr     s    
HOOHX^^$4.6.E.E58***2
N
HNN#6(--
8&&s+#66<B
?
h!!#&k(//1B!	Er1   c                B   t        j                  d      }|j                  ddd       |j                  dd	       |j                  d
dd       |j                  |       }t	        j
                  t        j                  d       |j                  rdj                  |j                        g}nt        }|D ]d  }t        ||j                        }|j                  rt        |j                                @t        d|        t        dt        |              f y)Nz,IDS Phase 6 natural-language router (smoke).)descriptionr   *z&Prompt to route. Omit to run demo set.)nargshelpz--callerr   )defaultz--json
store_truez$Emit JSON instead of formatted text.)actionr   z%(message)s)levelformat )r   z> z  r   )argparseArgumentParseradd_argument
parse_argsloggingbasicConfigINFOr   joinr   r   r   r*   printr0   r   )argvparserargspromptspr   s         r/   mainr     s    $$1_`F
2Z[

M:
<bcT"Dgll=A {{88DKK()' 54;;/99(""$%Bqc(OB'12345 r1   __main__)r   r   r   intr2   Tuple[str, Optional[str]])r   r   r2   r  )r   r   r   r   r2   None)r   r   r2   r   )r   r   r2   zTuple[str, float, dict])r   r   r2   r   )r   )r   r   r   r   r2   r   )r   r   r2   r   r   )r   zOptional[Sequence[str]]r2   r  )/r6   
__future__r   r   r,   r   r*   r   r   typingr   r   r   r   	getLoggerr   r   r7   r   r   r   r   r   SLA_OUTPUT_MS	dataclassr   RuntimeErrorr:   rX   rk   rs   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r3   
SystemExitr8   r1   r/   <module>r     s   #       6 6g-.  "++[!\ Y \    d#X X $X(W< WEP Pf L8<:1=B 	t 	!o 	,( 	2	* %+*%	t 

 "*0CY~*  2 z
TV
 r1   