
    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	Z	ddl
mZ ddlmZmZ ddlmZ ddlmZmZmZ ddlmZ  ej,                  e      Zd	Zd
ZdZ ej8                         dk7  ZerddlZi Zde d<    ejB                         Z"d"dZ#ed#d       Z$d$dZ%d%dZ&d&d'dZ'd&d(dZ(d&d)dZ)d*dZ*d+dZ+d,dZ,dd	 	 	 	 	 	 	 d-dZ-ddd	 	 	 	 	 	 	 	 	 d.dZ.d/dZ/ddd	 	 	 	 	 	 	 d0dZ0	 	 	 	 	 	 	 	 	 	 	 	 d1d Z1d'd!Z2y)2u  기획서 project-level 분석 service.

기획서는 project-level 자원이므로 분석 결과도 episode 와 무관하게
``projects/{project_id}/checkpoints/planning_doc_analysis/manifest.json`` 에 저장.

기존 ``PlanningDocAnalysisStep`` (StepRunner) 은 episode 단위 cascade 호환을
위해 유지하되, 본 service 가 만든 project-level checkpoint 가 있으면 그대로
mirror (LLM 재호출 없음).

업로드 직후 백그라운드 dispatch 로 사용:

    from app.services.planning_doc_analysis_service import (
        dispatch_planning_doc_analysis_async,
    )
    dispatch_planning_doc_analysis_async(project_id)

## race 안전 — per-project lock + multi-layer guard

핵심 race: 업로드 → 분석 시작 → 사용자가 즉시 delete + 재업로드 시, 이전
background thread (A) 가 늦게 끝나서 새 SOT (B) 를 덮어쓰는 시나리오.
status write / manifest promote / clear / dispatch queued write 가 모두
"check-then-act" 라 단순 atomic write 만으로는 race window 가 남음:

    A: is_current_job(A) → True (status=A)
    B: clear → dispatch → status=B
    A: atomic_write status={state:running, job:A}  ← B 의 도장 덮어씀

근본 해법은 critical section 직렬화. ``_project_lock(project_id)`` 가
fcntl.flock + threading.Lock combo 로 same/cross-process 모두 보호.
LLM call 은 길어서 lock 밖 — write/check/cleanup 만 lock 안.

추가 안전망 (lock 으로 닫지 못한 edge — 옛 schema, race-free 호출 등):

  L1. ``_write_status``/``_write_manifest`` 의 ``expected_job_id`` 가드 —
      lock 안에서 호출되면 redundant 한 double check (안전 차원).
  L2. manifest payload 에 ``job_id`` + ``source_hash`` 동봉 → reader 검증.
  L3. promote = staging file (``manifest.{job_id}.json``) 에 atomic write
      후 lock 안에서 ``os.replace`` — cross-job manifest.json 보호.
  L4. ``load_project_checkpoint`` 가 manifest.source_hash 와 현재 disk
      source hash 비교 → 다르면 reject (cleanup unlink 제거 — reader 의
      늦은 unlink 가 fresh manifest 를 죽이는 race 차단). 다음 dispatch
      promote 또는 clear 가 stale 자연 정리.
    )annotationsN)contextmanager)datetimetimezone)Path)DictIteratorOptional)settingszstatus.jsonmanifest.jsonz.lockWindowszDict[str, threading.Lock]_thread_locksc                    t         5  t        j                  |       }|t        j                         }|t        | <   |cd d d        S # 1 sw Y   y xY wN)_thread_locks_guardr   get	threadingLock)
project_idlks     `/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/planning_doc_analysis_service.py_get_thread_lockr   S   s?    	z*:!B(*M*% 
		s   6AAc           	   #    K   t        |       }|j                          d}	 t        ret        |       }|j	                  dd       |t
        z  }t        |d      }t        j                  |j                         t        j                         d |D	 t        j                  |j                         t        j                         	 |j                          |j!                          y# t        $ r$}t        j                  d| dd |       Y d}~Jd}~ww xY w# t        $ r Y Lw xY w# |	 t        j                  |j                         t        j                         n0# t        $ r$}t        j                  d| dd |       Y d}~nd}~ww xY w	 |j                          n# t        $ r Y nw xY w|j!                          w xY ww)uK  per-project critical section — same/cross process 모두 직렬화.

    threading.Lock + fcntl.flock combo:
      - threading.Lock: 같은 process 내 thread 들 빠르게 직렬화.
      - fcntl.flock (POSIX): uvicorn workers / --reload 일시 중첩 등
        cross-process race 방어 (file descriptor advisory lock).

    LLM call 같은 long-running operation 은 lock 밖에서 호출 — write /
    check / cleanup 만 critical section. 분석 자체가 lock 잡으면 사용자
    재업로드 시 새 dispatch 가 이전 분석 끝날 때까지 막히는 UX 손해.
    NTparentsexist_okwz2_project_lock fcntl unlock failed (project=%s): %s   )r   acquire	_IS_POSIXget_project_checkpoint_dirmkdir
_LOCK_FILEopenfcntlflockfilenoLOCK_EXLOCK_UNOSErrorloggerwarningcloserelease)r   tlockfd	lock_pathexcs         r   _project_lockr4   \   sm     Z(E	MMOA*:6AGGD4G0JIY$AKK
EMM2=AHHJ6	 	  HrNC    =AHHJ6 HrNC 
	 s   GA/D& G2C' D G'	D0D
GDG	D# G"D##G&F?*2EF?	F
&F F?F

F?FF?	F+(F?*F++F??Gc                F    t        t        j                        | z  dz  dz  S )u<   project-level planning_doc_analysis checkpoint 디렉토리.checkpointsplanning_doc_analysis)r   r   projects_dir)r   s    r   r!   r!      s1     	X""#
	
	 "	"    c                   | j                   j                  dd       | j                  | j                  dt	        j
                         j                  dd  dz         }	 |j                  t        j                  |dd	      d
       t        j                  t        |      t        |              y# t        $ r3 |j                         r!	 |j                           # t         $ r Y  w xY w w xY w)u  temp file + os.replace 로 atomic JSON write.

    부분 쓰기/동시 read 에 의한 corrupt JSON 방지. tmp 파일에 uuid 접미사를
    붙여 동시 다중 thread 충돌을 회피 (현재는 job_id 가드로 단일 writer 가
    보장되지만 안전 차원 dual layer).
    Tr   .Nr   z.tmpF   )ensure_asciiindentutf-8encoding)parentr"   with_suffixsuffixuuiduuid4hex
write_textjsondumpsosreplacestr	Exceptionexistsunlinkr*   )pathpayloadtmps      r   _atomic_write_jsonrT      s     	KKdT2


4;;1TZZ\-=-=bq-A,B$)GG
HCJJwU1= 	 	
 	

3s8SY' ::<

 	  s1    AB2 2C.CC.	C)&C.(C))C.c                x   t        j                         }t        t        j                        | z  dz  dz  }|j                         rR|j                  d       	 |j                  d      5 t        fdd      D ]  }|j                  |        	 ddd       |j                  d       d}|ddlm}  |       }d}	 ddlm} |j!                  |      j#                  |j$                  | k(        j'                         }	|	r|	j(                  xs dnd}
|j                  |
j+                  d             |r|j-                          |j/                         S # 1 sw Y   xY w# t        $ r6}t        j                  d| dd	 |       |j                  d
       Y d}~d}~ww xY w# |r|j-                          w w xY w)uT  기획서 source 의 결정적 hash.

    PDF bytes (streaming) + 'PDF_END|TEXT:' separator + planning_doc_text
    의 sha256 결합. PDF 가 없거나 text 가 비어 있어도 hex digest 가 항상
    return (empty source 도 결정적 값).

    reader/writer 가 같은 입력으로 같은 hash 를 만들도록 separator 명시.
    assetsplanning_doc.pdfs   PDF:rbc                 &     j                  d      S )Ni   )read)r0   s   r   <lambda>z%compute_source_hash.<locals>.<lambda>   s    !&&-r9   r9   Nz5compute_source_hash: PDF read failed (project=%s): %sr   s   <read-error>s   |TEXT:Fr   SessionLocalTProjectRegistry r?   )hashlibsha256r   r   r8   rO   updater$   iterr*   r+   r,   app.core.databaser]   app.models.catalogr_   queryfilteridfirstplanning_doc_textencoder-   	hexdigest)r   dbhpdf_pathchunkr3   own_dbr]   r_   projecttextr0   s              @r   compute_source_hashru      sy    	AX""#j08;>PP  			&t$!"7=EHHUO > % HHYF	z2^6HH_%VO&&*45UW 	
 5<))/R	W%&HHJ;;=7 %$  	&NNG2A HH_%%	&, HHJ s=   E" +&EE" ?A5F$ EE" "	F!++FF!$F9c                   t        | |      }|#|j                  d      s|j                  d      ryt        t        j                        | z  dz  dz  }|j                         ryd}|dd	lm}  |       }d}	 dd
lm	} |j                  |      j                  |j                  | k(        j                         }|r?t        |j                  xs dj!                               dk\  r	 |r|j#                          yy|r|j#                          	 t        t        j                        | z  dz  dz  }|j                         r-|j%                         D ]  }	|	dz  dz  j                         s y y# |r|j#                          w w xY w)u  기획서 존재 여부 단일 판정 — 어느 한 곳이라도 있으면 True.

    검사 순서 (우선순위):
      1. project-level checkpoint — ``load_project_checkpoint`` 로 검증된
         data (source_hash 일치) + ``available_sections`` 비어 있지 않을 때만.
         empty/stale/error manifest 는 false-positive 차단.
      2. PDF on disk (assets/planning_doc.pdf)
      3. ProjectRegistry.planning_doc_text (≥100 chars)
      4. legacy episode-level checkpoint (옛 프로젝트 호환)

    db: caller 세션. None 이면 SessionLocal 일회 사용.

    `_if_planning_doc` (applicability) 와 `_project_has_planning_doc`
    (dispatcher) 가 동일 판정을 공유하도록 단일 source.
    rn   available_sections
charactersTrV   rW   Fr   r\   r^   r`   d   r6   episodesr7   r   )load_project_checkpointr   r   r   r8   rO   re   r]   rf   r_   rg   rh   ri   rj   lenrk   stripr-   iterdir)
r   rn   cp_datarp   rr   r]   r_   rs   legacy_baseep_dirs
             r   project_has_planning_docr      sp   $ &jR8G;;+,L0I 	X""#j08;>PP   F	z2^6HH_%VO&&*45UW 	
 sG55;BBDELHHJ HHJ 	X""#j0=@:M  !))+F00?BJJL ,  HHJ s   :A-E/ /Fc                   t        |       t        z  }|j                         sy	 t        j                  |j                  d            }t        |t              sy|j                  d      }t        |t              r7|r5	 t        | |      }||k7  r"t        j                  d	| dd |dd
 |dd
        y|j                  d      }t        |t              r|S dS # t        t        j                  f$ r }t        j                  d|       Y d}~yd}~ww xY w# t        $ r&}t        j                  d| dd |       |}Y d}~d}~ww xY w)uY  저장된 project-level 분석 결과 — source_hash 검증 통과 시에만 반환.

    검증 로직 (L4 — stale resurrection 최종 안전망):
      - manifest.source_hash 가 있고 현재 disk source 의 hash 와 다르면
        stale (delete 후 다른 source 로 재업로드 race 등) → ``None`` reject.
        **cleanup unlink 는 의도적으로 하지 않음** — hash 계산 도중 다른
        thread 가 fresh manifest 를 promote 하면, reader 가 fresh 를 unlink
        하는 race 가 생김. disk 정리는 다음 dispatch promote (덮어쓰기) 또는
        ``clear_project_checkpoint`` 가 담당.
      - manifest.source_hash 가 없으면 (옛 schema) 검증 skip + 정상 read.

    옛 schema (source_hash 없는 manifest) 는 한 번 정상 read 된 후, 새 분석
    트리거 시 새 schema 로 덮어씀.
    Nr?   r@   z/planning_doc project checkpoint read failed: %ssource_hashrw   ud   planning_doc source_hash compute failed (project=%s): %s — skipping reader hash check (read as-is)r   uo   planning_doc manifest source_hash mismatch (stale): project=%s manifest=%s current=%s — rejecting (no unlink)   data)r!   _MANIFEST_FILErO   rI   loads	read_textr*   JSONDecodeErrorr+   r,   
isinstancedictr   rM   ru   rN   )r   rn   cprawr3   manifest_hashcurrent_hashr   s           r   r|   r|     s:    
$J	/.	@B99;jjw78 c4 GGM*M-%-	).zbAL L( NNC2Acr 2L"4E
 776?DdD)43t3C T))* H#N  	)NN:2A
 )L	)s/   %C ?D D1DD	ED>>Ec                8   t        |       t        z  }|j                         sddiS 	 t        j                  |j                  d            S # t        t        j                  f$ r3}t        j                  d| dd |       dd	t        |      d
cY d}~S d}~ww xY w)u   현재 분석 진행 상태. {state: idle|queued|running|done|error, ...}

    corrupt JSON 은 silent ``idle`` 대신 ``error/status_unreadable`` 로
    surface — 운영자가 disk 손상을 인지하도록.
    stateidler?   r@   uG   planning_doc status read failed (project=%s): %s — surfacing as errorNr   errorstatus_unreadable)r   r   detail)r!   _STATUS_FILErO   rI   r   r   r*   r   r+   r,   rM   )r   r   r3   s      r   load_statusr   I  s     
$J	/,	>B99;  Tzz",,,899T))* TUrNC	
 !+>#c(SSTs   $A B&(BBBc                >   t        |       t        z  }|j                         sy	 t        j                  |j                  d            }t        |t              r%|j                  d      }t        |t              r|S dS y# t        t        j                  f$ r Y yw xY w)u?   현재 disk status.json 의 job_id (없거나 손상 시 None).Nr?   r@   job_id)r!   r   rO   rI   r   r   r*   r   r   r   r   rM   )r   r   r   vs       r   _read_status_job_idr   \  s    	#J	/,	>B99;jjw78 #tGGHq#&q0D0 T))* s   %B   BBc                (    |yt        |       }||k(  S )u   job_id 가 현재 status.json 의 job_id 와 일치하면 True.

    job_id=None 으로 호출되면 guard 를 비활성 (sync 직접 호출용).
    T)r   )r   r   currents      r   _is_current_jobr   k  s!    
 ~!*-Gfr9   expected_job_idc                   |1t        | |      s%t        j                  d| dd |t        |              yt	        t        |       t        z  |       y)u   status.json atomic write. expected_job_id 가 주어지면 현재 disk
    job_id 와 일치할 때만 쓰고 stale 이면 drop.

    Returns True iff 실제로 write 됨.
    NzOplanning_doc status write dropped (stale job_id): project=%s self=%s current=%sr   FT)r   r+   infor   rT   r!   r   )r   rR   r   s      r   _write_statusr   v  sT     "?:+W!rNO-@-L	

 1*=LgVr9   r   r   c               H   t        |       t        z  }|||t        j                  t        j
                        j                         d}|t        ||       |S t        | |      s%t        j                  d| dd |t        |              y|j                  d| dz  }t        ||       t        | |      s6t        j                  d| dd |t        |              	 |j                          y	 t#        j$                  t'        |      t'        |             |S # t        $ r Y yt        $ r!}t        j!                  d||       Y d}~yd}~ww xY w# t        $ rK}t        j!                  d	| dd |       	 |j                          n# t        t        f$ r Y nw xY wY d}~yd}~ww xY w)
u{  manifest 를 staging file → promote 패턴으로 atomic 게시.

    이전의 "write-then-conditional-unlink" 는 race window 가 있었음:
        A: status check OK → atomic replace manifest.json (A data)
        B: 그 사이 clear + 새 dispatch + write manifest.json (B data)
        A: status check FAIL → unconditional out.unlink() → **B 의 fresh
            manifest 까지 삭제**

    staging 패턴:
      1. ``manifest.{job_id}.json`` 에 atomic write (job-isolated)
      2. status.job_id 가 자기 것인지 재확인
         - 자기 것 → ``os.replace(staging, manifest.json)`` 으로 promote
         - 아니면 → 자기 staging 만 unlink (manifest.json 은 건드리지 않음)
      3. promote 후 동시 다른 job 이 새 promote 해도 자기 staging 은 이미
         소비된 후 → 안전. manifest.json 의 owner 는 마지막 promote 가 결정.

    L1: 1차 status 가드 (early bail-out — staging write 비용 절약).
    L2: payload 에 ``job_id`` + ``source_hash`` → reader 가 L4 검증 가능.
    L3: promote 직전 status 재확인 + cross-job manifest.json 보호.

    expected_job_id=None 호출 (sync 직접 caller) 은 race-free 가정 → 단순
    atomic write. caller 는 자기 책임으로 호출 직전에 status/source 정합성
    확인.

    Returns 실제로 promote 된 Path (drop / staging-only cleanup 시 None).
    )r   r   r   generated_atNuO   planning_doc manifest write dropped (1차 stale): project=%s self=%s current=%sr   z	manifest.z.jsonuQ   planning_doc manifest promote dropped (2차 stale): project=%s self=%s current=%su.   planning_doc staging cleanup failed: %s — %su7   planning_doc manifest promote failed: project=%s — %s)r!   r   r   nowr   utc	isoformatrT   r   r+   r   r   rB   rP   FileNotFoundErrorr*   r,   rK   rL   rM   )r   r   r   r   outrR   stagingr3   s           r   _write_manifestr     s   B %Z
0>
AC!" X\\2<<>	G 3(
 :7]rNO-@-L	
 jjY&7u==Gw(
 :7_rNO-@-L	
	NN 


3w<S* J+ ! 	
 	  	NN@'3  		  	ErNC	
	NN!7+ 			sZ   D ,(E 	E
!E
)EE
	F!F1FFFFFFF!c                *   t        |       5  t        |       }|j                         s
	 ddd       yt        t        fD ])  }||z  }|j                         s	 |j                          + |j                  d      D ]'  }|j                  t        k(  r	 |j                          ) 	 ddd       y# t        $ r!}t        j                  d||       Y d}~d}~ww xY w# t        $ r!}t        j                  d||       Y d}~d}~ww xY w# 1 sw Y   yxY w)u  기획서 교체/삭제 시 호출 — manifest+status+staging file 모두 제거.

    staging file (``manifest.{job_id}.json``) 는 in-flight 분석이 promote
    하기 전 임시 파일. clear 시 이전 dispatch 의 잔재를 같이 청소.

    ``_project_lock`` 안에서 수행 — 동시에 발생하는 dispatch / status write
    /  promote 와 직렬화되어 partial state (예: status 만 남고 manifest 잔존)
    가 노출되지 않음.
    Nu+   clear planning checkpoint failed: %s — %szmanifest.*.jsonu(   clear planning staging failed: %s — %s)r4   r!   rO   r   r   rP   r*   r+   r,   globname)r   r1   r   pr3   r0   s         r   clear_project_checkpointr     s     
z	"&z2xxz 
#	" $\2DDAxxzZHHJ	 3 )*Avv'S
	 + 
#	"  ZNN#PRSUXYYZ  SI1cRRS# 
#	"sd   D	$D	B/(*D	C#D	/	C8CD	CD		D%D<D	DD		Dr   	queued_atc               n   ddl m} ddlm}m}m} ddlm} ddlm	} ddl
m}	 t        j                  t        j                        j!                         }
	 t#        | |||
|||||||	      S # t$        $ r}	 t'        |       5  t)        | d|||
t        j                  t        j                        j!                         t+        |      d	|
       ddd       n# 1 sw Y   nxY wn0# t$        $ r$}t,        j/                  d| dd |       Y d}~nd}~ww xY wt,        j/                  d| dd |        d}~ww xY w)u  동기 실행 — 기획서를 LLM 으로 분석 + project-level checkpoint 저장.

    구조: write/check 만 ``_project_lock`` 안에서 직렬화, LLM call 은 lock
    밖. concurrent dispatch / clear 가 status SOT 를 뒤집어도 자기 phase
    시작 시 ``_is_current_job(job_id)`` 로 안전하게 bail out.

    Phase 흐름:
      1. **lock**: running status 마킹 + bail-out 체크 + LLM input 수집
         (PDF bytes, text, source_hash)
      2. **lock 밖**: LLM call (PDF multimodal → text fallback)
      3. **lock**: bail-out 체크 + manifest staging→promote + done/error
         status 마킹

    job_id: dispatch 시 발급된 식별자. ``None`` 이면 ``_is_current_job`` /
        ``_write_*`` 의 stale-write guard 만 비활성화됨 (caller 가 race-free
        보장한다는 가정). ``_project_lock`` 자체는 항상 유지 — 동시 sync
        호출이나 clear 와 SOT mutation 직렬화는 그대로 보장.
    queued_at: dispatch 시점 timestamp (status payload 에 보존).
    r   r\   )_ANALYSIS_SCHEMA_EMPTY_RESULT_SYSTEM_PROMPTr^   )call_structured)load_project_llm_config)
r   r   
started_atsession_localschemaempty_resultsystem_promptproject_registry_clsr   r   r   r   r   r   r   finished_atr   r   Nz@planning_doc_analysis error-status write failed (project=%s): %sr   z-planning_doc_analysis (project=%s) failed: %s)re   r]    app.core.steps.planning_doc_stepr   r   r   rf   r_   app.modules.llm.llm_clientr   &app.services.analysis_dispatch_servicer   r   r   r   r   r   !_run_planning_doc_analysis_phasesrN   r4   r   rM   r+   	exception)r   r   r   r]   r   r   r   r_   r   r   r   r3   e2s                r   run_planning_doc_analysis_syncr     s+   4 / 
 3:Nhll+557J(0!&#&(!0+$;
 	
  	z*!("(%.&0'/||HLL'A'K'K'M!$S %+ +**  	R2A 	
 	;rNC	
 	5sU   A+ +	D45C$ AC	C$C 	C$#D/$	D-DD/DD//D4c       
        	    |       }	 t        |       5  |Kt        | |      s?t        j                  d| dd |t	        |              |cddd       |j                          S |j                  |      j                  |j                  | k(        j                         }|r|j                  nd}t        t        j                        | z  dz  dz  }|j                         } |
||       }t        | |      }t!        | d|||d|	       |s|rt#        |j%                               d
k  rt        j                  d| dd        t'        | |||       t!        | d|||t)        j*                  t,        j.                        j1                         dd|	       |cddd       |j                          S |r|j3                         }nd}ddd       |j                          d}iddl}|j7                  |      j9                  d      }dddddd| idg}t        j                  d| dd t#        |             	  |	d|||xs dd      }|XrVt#        |j%                               d
k\  r:d| }t        j                  d| dd t#        |              |	d|||xs dd      }t        |       5  |?t        | |      s3t        j                  d | dd |t	        |              ||n|cddd       S |wt        j=                  d!| dd        t'        | ||       t!        | d"|||t)        j*                  t,        j.                        j1                         d#d$|	       |cddd       S dd%lm }  ||      }||d&<   t'        | ||      }||cddd       S t!        | d|||t)        j*                  t,        j.                        j1                         t#        |jC                  d'g             t#        |jC                  d(g             |d)|	       t        j                  d*| dd t#        |jC                  d'g             t#        |jC                  d(g             ||       |cddd       S # 1 sw Y   xY w# |j                          w xY w# t:        $ r!}t        j=                  d|       Y d}~hd}~ww xY w# 1 sw Y   yxY w)+u   run_planning_doc_analysis_sync 의 본체 — outer try/except 가 status
    error 마킹을 보장하므로 본 함수는 정상 흐름만 책임.
    NzPplanning_doc_analysis bail at phase 1 (stale job): project=%s self=%s current=%sr   rV   rW   rw   running)r   r   r   r   r   rz   uJ   planning_doc_analysis (project=%s): 기획서 없음 — 빈 결과 저장r   doneT)r   r   r   r   r   emptyr   r?   rt   uI   이 기획서 PDF를 분석하여 구조화된 정보를 추출하세요.)typert   	image_urlurlzdata:application/pdf;base64,)r   r   z=planning_doc_analysis (project=%s): PDF multimodal (%d bytes)r7   g?)stepr   user_promptresponse_schemaproject_configtemperaturez/PDF multimodal failed, falling back to text: %su_   다음 기획서를 분석하여 구조화된 정보를 추출하세요.

## 기획서 전문

z<planning_doc_analysis (project=%s): text fallback (%d chars)uc   planning_doc_analysis bail at phase 3 (stale job): project=%s self=%s current=%s — discard resultz<planning_doc_analysis (project=%s): both PDF and text failedr   u!   PDF 와 text 분석 모두 실패r   )compute_available_sectionsrx   ry   key_relationships)r   r   r   r   r   ry   relationshipssectionsz`planning_doc_analysis (project=%s) done: %d characters, %d relationships, sections=%s, job_id=%s)"r4   r   r+   r   r   r-   rg   rh   ri   rj   rk   r   r   r8   rO   ru   r   r}   r~   r   r   r   r   r   r   
read_bytesbase64	b64encodedecoderN   r,   r   r   r   )r   r   r   r   r   r   r   r   r   r   r   rn   rs   planning_textrp   has_pdfproject_llm_configr   	pdf_bytesresultr   pdf_b64r   r3   user_prompt_textr   computedwrotes                               r   r   r   W  s    $ 
BI:&!/*f*M4rNF,?
,K
 $ '&P 	
} -.,//:=> 
 :AG55dMX**+ %%  oo'G "9Z!H-jR@K&$!*",	 !'	 !S)<)<)>%?#%E`rN  $* !'"(%.&0'/||HLL'A'K'K'M!% %+ $} '&P 	
 $//1	 	M 'P 	
 F""9-44W= c
 $9'C
 	KrNC	N	
	$,+' &19TF 	##%&#-&&3_6 	 	JrNC.	

 !('("-5
 
z	"oj&&IKKC2A(;J(G
 $/6\ 
#	" >NNN2A L &K $$!*",#+<<#=#G#G#I@ !'  ? 
#	"J	
 .f5'/#$"
 =g 
#	"h 	 &('||HLL9CCE!&**\2">?!$VZZ0CR%H!I$	 #	
 	4rN

<,-

.34	
 W 
#	"K '&P 	
@  	NNA3 	: 
#	"ss   Q7 4Q*	Q7 "D3Q*	Q7 /Q*Q7 7R .8R90A/R9)%R9CR9*Q4/Q7 7R		R6R11R69Sc           	         t        j                         j                  t        j                  t
        j                        j                         t               5  t         dd       ddd        fd}t        j                  |d dd  ddd  d	      }|j                          S # 1 sw Y   FxY w)
uy  업로드 직후 백그라운드 thread 로 분석 실행 + ``job_id`` 반환.

    동작:
      - ``_project_lock`` 안에서 uuid ``job_id`` 발급 + ``status.json`` 에
        ``{state:queued, job_id}`` 도장. concurrent dispatch 와 직렬화되어
        마지막 dispatch 의 job_id 가 SOT 가 됨.
      - background thread 는 자기 ``job_id`` 를 들고 ``run_..._sync`` 호출.
        phase 마다 자기 job_id 가 still current 인지 lock 안에서 확인.

    응답 지연 회피 + 결과는 ``checkpoints/planning_doc_analysis/manifest.json``.
    진행 상태는 ``status.json`` 으로 polling 가능.
    queued)r   r   r   Nc                 |    	 t               y # t        $ r } t        j                  d|        Y d } ~ y d } ~ ww xY w)Nr   z3planning_doc_analysis background thread crashed: %s)r   rN   r+   r   )r3   r   r   r   s    r   _runz2dispatch_planning_doc_analysis_async.<locals>._runV  s?    	*6Y  	LLEs 	s    	;6;zplanning_doc_analysis-r   -T)targetr   daemon)rE   rF   rG   r   r   r   r   r   r4   r   r   Threadstart)r   r   tr   r   s   `  @@r   $dispatch_planning_doc_analysis_asyncr   >  s     ZZ\FX\\*446I 
z	"j"#
 	 
#	 	%j!n%5QvbqzlC	A
 GGIM1 
#	"s   B44B=)r   rM   returnzthreading.Lock)r   rM   r   zIterator[None])r   rM   r   r   )rQ   r   rR   r   r   Noner   )r   rM   r   rM   )r   rM   r   bool)r   rM   r   zOptional[Dict])r   rM   r   r   )r   rM   r   Optional[str])r   rM   r   r   r   r   )r   rM   rR   r   r   r   r   r   )
r   rM   r   r   r   r   r   r   r   zOptional[Path])r   rM   r   r   )r   rM   r   r   r   r   r   r   )r   rM   r   r   r   r   r   rM   r   rM   r   r   )3__doc__
__future__r   ra   rI   loggingrK   platformr   rE   
contextlibr   r   r   pathlibr   typingr   r	   r
   app.core.configr   	getLogger__name__r+   r   r   r#   systemr    r%   r   __annotations__r   r   r   r4   r!   rT   ru   r   r|   r   r   r   r   r   r   r   r   r    r9   r   <module>r     s  *X #    	    % '  + + $			8	$  
 HOO*	 ,.( -$inn&  $ $N0+\:z54pT& &*	 #	
 
4 &*!%XX
X #	X
 X XvSD !#	NN N 	N
 
Nbdd d 	d
 d d 
dN)r9   