From d8b21ea679c79d0edd21928fc93a9cc60e20cead Mon Sep 17 00:00:00 2001 From: phala-agent Date: Sat, 30 May 2026 07:51:42 +0000 Subject: [PATCH] feat: add liteparse template --- templates/config.json | 28 ++ templates/icons/liteparse.png | Bin 0 -> 41321 bytes templates/prebuilt/liteparse/README.md | 147 +++++++ .../prebuilt/liteparse/docker-compose.yml | 377 ++++++++++++++++++ 4 files changed, 552 insertions(+) create mode 100644 templates/icons/liteparse.png create mode 100644 templates/prebuilt/liteparse/README.md create mode 100644 templates/prebuilt/liteparse/docker-compose.yml diff --git a/templates/config.json b/templates/config.json index 4d013eb0..f0421bb2 100644 --- a/templates/config.json +++ b/templates/config.json @@ -4463,6 +4463,34 @@ }, "tags": ["Media & Design", "Automation", "AI Agents"] }, +{ + "id": "liteparse", + "name": "run-llama/liteparse", + "description": "A fast, helpful, and open-source document parser", + "repo": "https://github.com/Phala-Network/phala-cloud/tree/main/templates/prebuilt/liteparse", + "author": "run-llama", + "icon": "liteparse.png", + "envs": [ + { + "key": "LITEPARSE_SERVER_IMAGE", + "required": false, + "default": "ghcr.io/run-llama/liteparse-server:main", + "description": "Upstream LiteParse server image used by the template. Pin to a digest or known tag for stricter production reproducibility." + }, + { + "key": "LITEPARSE_DEMO_TEXT", + "required": false, + "default": "LiteParse Phala Cloud demo", + "description": "Text embedded in the generated demo PDF and checked by the /demo endpoint." + } + ], + "defaultResource": { + "vCPU": 1, + "memory": 2048, + "diskSize": 20 + }, + "tags": ["AI Apps & Workflows", "Developer Tools", "Data & Storage"] + }, { "id": "markitdown", "name": "microsoft/markitdown", diff --git a/templates/icons/liteparse.png b/templates/icons/liteparse.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ed95da742a2c14e07a4ff22702a55e726989ad GIT binary patch literal 41321 zcmXt=bzGD0*T)CajdXXXgmibeD2+;q(lK&KcL<2&2$7N$q#2DO4T6N!=pMBJgFW;6 zdp&=R*Vuhs*L~uBKIdkK1Y<)jVgh;s002O&qpfZV0AQkj#BjyKLBFs`sTl(RC89d& zD(1kY<9oMEu0?;Q_`2>it=G@ylj$=olr0PJ$>lD~F4 zyiZPyKl}#nzHWZnjRJ-0H%du_j=%3;c%c%}ai#zbCHr0zx&w74w&YrlHV$a1?0TsS zn0r6d<0;vHQ?#*?K&x(@_BSR;x%CwJyAb-V<&wZVfMw-<>+z_2^=?S&n7M*h>B;=s z8Zvi&`6~1Yr@XbU{0jt8d-rwzIFHlhPrx~H{R@h1|)82P^dzft7 z%--8?5jatO4x+S*x}uoq7b2V}a=^bgLbDnaQe6K3y*|8%e;*pVlUljiBSA_#_Rwce zpD?x|Le)JELn!Mz|4@W!DOyUHRpmnj5~H3yF;hCml3sAHJl66(k@lP1DeIP_Q>=~^ zI(;Zk3Zt7t&N^M5F)=NB__f}%m1RQ+lURhv0ZVfetl2R-C9HXCEPUh59)r;3>!}oL zz(`WX+O9GsvgBQuT>Eovi;-jLjuDd)+{yq;Je%%em^v@xXoI~+SJwl5bk9$}uZ)U3 z*6)ef;z%Z?;oE@qITTN^??P@VD|t2A-B=R#`90s0E`;}7Mx zk)2mDzhRvXk*Gr%1o4OD+nyx6cOX_C1>_tMV@p5q+cg6@_BXk?o zzT*UROrHNm)U;TU(B;k@(Q4vuG_l0bFrB}Z#EYa;PmaU##Lh=#HgK%6u$~ zi#a9Q$Ixv@UUk9;+HLvM8QG%o)YUW1omi`&KL+eR}N!#&3@E)e)bhwJ0O2&O@BrC|+OJzfXZ0 z)=OBVnF$PoB{^}bG(J%oV_u9xdOOkMGiKmbV!@QR+xvIzNBp=(nQ^no27T|!W<;|9xLA=}QUTS?#^e=vfRLzgn z(Zp=R<@3u%F4o09INg*TWtzZlA4bX#fQt(1ftQKll!Sr zEwjMQ5$NCIViZ!(mZVi>Juwb3(ZMjksHq6CDc)e3TvI7z%&e0OwbQBguc4U}HO!KW z2;Fh-lGJQnRjUv`t!hcx$es7{tm;me{Mi?9t~qvsj(eG}WK%lP{_Y-Mw+j!lo`2aroZtYsbWYsa4r%XsXsHd$d;ws!R^J96 zwVkOdzc%$NcR7bys*93@^*`$8(RA7?N7xIx?_L5vg5>%bCTw>Z#s6^wTb!nM-y zdsufug;hmpLDD#jdx|H4H@Yrjn1PxE>qu?oG=g@rirZu(ZeB(_o za%6c$nyTuLSYew=%=Q=F)%~qe9`MDQ+D+kyq|(+vW0%nM%`L_Y|GyXD&J^YyIvmn> zZEF0pj#_-XIn_XG&LkS*(d(W}?9poRg|~bf=5u>qLvm$56~9BKXHPPMj7<4QqZY9i_ZtjTwjIP%XhpBq{vGz*a1)mzOsBnz4~^yE@fEn+e8?=B_A zuF?SErgX!E6c*{bw!+*^hu;mkaLj9%2m3_Ze5oVER4nsv8i5|P|8fxZpZl2^_+qL* zY&vmzL;h>1=uBEry}i4ZL}1wbWAeWZAK%&9RK#RIAubM%kaV6Aro}UhmsqKL{bR!v zP)4Jigez9iH4_^5RYI8eN%ZTF<)k9h{od31_41NLZcO2-)O+PU@S^% zvSAgk3bl6jiA`!#$f>u*S914zq{=s`-%Mq>4lRx38@_xd|90Se^Tv=VTeCkiv$oq7 zccjMB5}RbHvCE5J-ycqp+2n6HmEW_E`@Y9h4talSk?6406Rm1dT}oA2i!DU~o0?Yr z>sNEDL)#EcA$dXhv}JBiq1d~*`li?NT1Da@<)l?Y^I({UgOcySRL6PQn0Z)D=GD;9 z))FZO%;wm0{3@@;(qxPRV3i1Pt&J>Z-k|qP)7DZ?;dkeviWTq=O&2CxpEqkM0a=YK zS{j^A&-c#;>?YK))*K4R0AsD)gmE;?7X9WmMf7Z4_-sh?6o!5Ig!Mj)i#U%_GQsUa zUQ_y&jMaU$9{6>!b#+Kc9>BlOs#-Em5=We)8%N2oJ@oq(t^+iz)JDDx=24_Z(_5?u zdZt(|dzq`}Z{M-*SkQRtcj8&~82e9@^+||zdF3CW{E+1>Rzkc3I`h6FtSO0h$>t6^30s8Iu`)$o9;G)tlN*lr}|0JXXdPIu9zNiaQ@ld-rCkFU_ooa z`C3($U%9+Gb+bJael$9G&FVKpQ;;0I}zTU(d;VG zOyy(Ii)(_NZj3+CwBWXrZ=|8Q7$PFiBJ;_2w_@8d1rM+ggl7^&wq*XC@l)HIT1c>p zv#Hy_-C(wARe30^_5|OKzjQ!iTwToM_Lsu644n~Ip)GE_VVWj z&boqQPqOS%!GfC4WvZH>9#sNUx4Ah9^)6S4MsDJ#0_WO>5?+&5a)1@9LyclpM{s@y zo^e}Rb%e2gji@%)oUW6EiXRHG@^*vC$!%IqCKM_eNNHeY9`V;y4?^i*^|_+i+of8( z#xH+XHIDg*{|H$`6P{P)>vDC(`N`oX+#>tv-07Tt|1o9eL2L~-Me^rWYHGX#Xnyl5 z?gDuqEe+O;1L2N9`o#v;tSaeatG^g#q-r_4rP;J4EZt36n8k@~E3F1DZpGM=T?ZFa zzV(E7zlm~8qd3*aJ;v)89G#!4wd+q(72FI#eE6#B^;QH*WU=*WHU%Z%HbcfeucI4_ ztM>+;@d@_iL5>&(ZVo{e1OA2za}scxkl6tPx>rNjl`q3B2I!UPmvRSgvnpOxs=T%O zX`yOxrf%lwXBXP$3}mgHX(<*X(6+MI$|LQQ4wDOh=3?gc%16284M~gXk>F^)B^S8? z`O;!``i>#U&A}`N@393w1yd1VO#I@bjeFRWq6F5dGtpgE;Wv)EL@AUe zG~LxVmR^?<(Z*yJ@;K})!%X(IMfZ71E?YWn9zR;%`OWKgZTW{PLWIRLSR%=szQhvN z=PX%`h2R**8Dba8m(edJK5ATO8hGu_QR9GmB`C07(eCu3-It3b=Y=YzgQBfA|L@A& zgHVCp%kO(XWaydMUefn?8U_ZGMZ|s4CfPQzPC4|THLwkNRyD0Z>R~SUM7?NsZmNAx zm6J)lPfp@cVTZ)1k73y_0V#-+Cqq{Lk(W}kl(`n}Y}X21!H<{3yCVvgN{f2;7q2^-@O(V5 z6Edi`gFB*Q;D6qnP|e56q)1S{7S%nLGOpJ9Y=}2wJkX+lZw1Na%APL#R-l_-~2Xv0$dQgW#BR_S!o|~YVT2-yzWn&$P^z>(hRGEPzuu$ z%&lPf2S{FYjbeWsHQs%`=jvOssQcK~rM)b&w|uv@ZSdz4)8;tl-%AA#p>>oJCG9Gw;MSh_ld{GLp5d%OYd!P4~sESL9 zBv!=_Q~toLb{bs86<4u~6$CPbyNd_Qxis<2aYD*YFE+*7%v>D<|zo z$ua(wPsf=|yR7*OR>2Z-QMzH#ORPe~*3oZ+AK`Nlt}if32k$8+z>*g(3zR6m3=N?;aC zNY-_`RyvHU{p@K|^LrkB*TW*T^xhcHDA-%$rG{kO%RV6VS7<_PTOxbTuY#X4eD$yW z*yfVF$|Pt@s|#vTe|P$%zr}JU?~2X-d)i{hnaxDto9Uv|9K z*?4M8e`9BrkhLXq8k4Uw6yc3K`^9-lx8G(L5*fQ|)tJ8crM(??=31j`xxPr?<)H~U zA9B<+DECems|Ocfxu|I8@Z>&ISeU;WmH**2Hrw#DFeRZ`U&Wj#TXN%YJIsjEC4 z2}{)E7jW4CFK7icF+-)@eWW63gpC8L{6%9tUX?IyU)1r%*kNJPQKe{1rt#j%yP6U~ zQHDG>!Vs%{^Hc*jw>J62HoA~kN0Vw_?4G6bf84yc-RGMtou~qS-VB=AqjlRG+9rP7 z@DOE<6@9Q^!V9m%`5Rx-_Elo47EgJ_tmbhZKeKkVD>bk_=VkX?PkX_lHysa$NO{l&0q?N6gN3J>(C%sfb$s%=J7VCff6P&_?= zNul5UCoD1Sm-%_YHyYdKH6?mIOx4z$1Y=dYY$Eqc(_tRxzmBi5L6l$IOgE3Z&$Z`T z*mCrk?4Q7?;`>W_{d&P-Z<6Pi* z5N8Oe?~Balgf+gh9&XWxHx>mycqkRj`TsnhyAuJwcP)?{!wwvJLWuLBI7_IYTIaz| zV)8;SD8Ij$73Hw|`ygoX;txWkOZn9jA3guk?x1>?2?YYvTZng+PBmNikAe0m+3(^_ zRlO2wo{lvrctY(u!9*}Ku_R4Sd~E|LNs(OwbFXvK4%5CLGmbc$2@>P$yQ_?D%A_d^ zdg12}%l%zsOsv^sr&r2BcR=~JzC7L!XtYybmRvP))LjxwAZdV9bqElydLv%O8WgST zrhaYArD3teEpEZ1`p&5*3Qx!T{_oU4(({C`dTiqcR}6O9%q=>hiO=3;e%bscuPn<} zuy~srNRWQ8w>VQe>?AK>qNS>yNB81)?wX{q8<-+~DO#Id{l0m;3HR%7zRE1p(y{_s zQX2D^L=r;E3>lK>8hzk4KJMwKIp(w%PPrQhzZW6p?mc+KH^Xer28%N$AUa zbMx$d_2x6j=z`uSn}3dc%N_EvPHnO&mHI0!Ij?7NX?m65mV9e;pPEtg9ueWi*Wh%1 z+Mo{Werefx#;MY5ZVg;#kDgT3UxdQPd)RVOJQnuvhjFR95)pOU3hG*Fce8>RpSrG+ z16BhH9$U3>i!bPBQ!_a33Xjjb1-H+0OMa`ZrDyjE9_RWwu6TqKECg(#UcU=y&@uxBsInKDi_ z`}dI6v&{ZbDeUg|#d0vrHYLr+{B*6~1S)p+9A3$PqED<}bx@QT!yQ{?N~wGi=LYP( z15#8T?Om1FPpHP4GHZsmxlV|F?IOvo_?+$GjsH^I3DIOvuIh=H@C|Ni9MZqbXe`_J z_Y8Qw@^TMha(rkK?aeDkjWI#Q#+OaaLFjIKij-~9$uqxXc;0g0T?;IG_E@>1T^mp87`k+pk5L)j$zviZn^Gwcl5*R^gI?Iy+Ir zWmda8-OoSqZcN6^^3yqdgx_T<2&!?r_S z-I-9W?f6C~gRoGhLcWNRlIf0sF8>4klMiEdSFD38%QfBZ-*IZOS^crxrsHUVL;8_v zZa`35>C*I2>9|htNV`mjHlIYs<4eos=Q6tIk$l zvPh^~ra)B0H*2%;$EHH{Fc6?^I$HC%QHi&tH0uz|T=q1Xrj`qkyKqYQ@#TT*C#bTl z)Gtw*A2Yd0>Et&r2J$sb#3I4W_#3QjMXj0B!|v9y018ZF%GRG*cvu6p7hA{BDbad| z9@l_UEWQ*+2g2;br5q-m=77dxwn)(l<-Pu~ROxVqFRt3WG%j_DL%)A~ntNvbX16X8 z@+-WGamKrR!N~AB^fXpCYjK5d8|z0!aqf>mP4)6IKgQ;D^#k6-wmEiipwYIWEvYVl z`AxRx7k~{%R<_8PIHk5*f1QNdW?z%fv1kYO|KX#9slCsBf{br-C;ASEJMG9PBhwNkmX!R?e-b)H%^vw#?&u z)vocBfzkQG7pMI?JEjgGX__4IcVkS#)p@vLQ!YlkuZ|U!N zjqc3w+$s**!l=~##+b9ZdDZos;;$)_KR;*tzj^?FPu^nAObsfP+?8ngNLe(uCYaf) zQCitzNG9wv4uziZT@dRGM6geA8=JNfUcAepCm8>*pc5v<-DFH_!e-X@_zPyIZWhPn z6-_wCLD+cR>m=8f>&__$u(Sjg`V?UQ0hL%wVE{ zbuR%6H_`MU(3L9Ht@^V>Tf^7D@i*^;Dom&z$G`wDF<4@TnNl=7v)*yU-zv{U#PM>{ z6W;DT|CkzDLMyqKJw@1H7CDN1{bXF*yBhf0`)xQLc#g5P>)Js-z@O3w?EZ}+??Z)! zNYFr%IlaYB8g=%5oB5KjiHy-HOUj>{ex$#7;@>v|uBEtD9+_}kThAv&yNc{%?mRyv zldWuyKG2eFvpJ(m^>(@*$J!jNY;ucVTWPUrkCri)RfZR#c2qlnONRL`V>_;7TQvm* zmejIb)SOdry)K0(U0zR;5t+u$s=#9xDVRwz z#mw{s#FO`@T zOrqdAoatnmuvfxr89MZl#}p48j?)Z zIYq(MkAC1J?9Hh>`>j7#$|QK!4pv}7e$4(H%*ORT)T+ecMgr9|y+}16{)?JAUDmf&YTRHq$upXQ@oc8i!(Pac*WTg#HP?l9jjnJ zbrG1<{`y*d+Ivj(xKv{tyT()MHM?{GTh&HpDigUZ_mLk>DL@$S&&{#E9BZX^6}*#q z5R02;*f-};tUN?AxLhJl)~qGQ-=9X==FiJu-ruLZ6b56 zCjx1-(U$Ffzd~-(-!OE;o<%2uKmM=avXNcVPVhw8M@TanLO50iQxxu* zBKVsFu)Mn5t|D0;*tf0M@e*T4;q8qVG5T55>cuM`oFXyHMIlza)ROFx0&2CXST);y(?T+zK^ul>=hR%tOqK9UG$qUmeuZ((=^Jb<{THgUt1;?AGt#N2=6 zLxp!vb_-k}r^uA$Fed2jhm`L{@w(%rfZ6V%yfGY0OyPDQ$ zF($b(H&ww^n+Y8TV^LXv!A#%h2!Hupk|lb}rLrT$IWfZ#Ds;lWG|U7BS18^g?b{*;+7 z_7)0k@1h@7TmYKR^r1hOY?Qm-1NL^dchfPZ+NNe>4@<%y}uX zMYdS&Pf~L4UlvCPwTP&Mn5qtGKbtOdE+sWTFe`x&zN+!R))iD`BUy4kF$zmi;V$TX{5tq?K)Rr2lzQ+Uw} zTz7aL{qQwvcWAIlmkySJO=TeNzWHs_^NJ43ZN8KKZ&u-_*X1tTu~(Ipy~tGrj=Gxv4Y# z>0vFecocP(#j|B=Aa(=Hi?iH~jRGIWb_o)^|X)G?~yCD|b zFH$h|Nmb(c2jaUXhRhLeZ;a;|nk4tx>E79gR$8254Oqwt(P-;@RWNpto-kec>oG=x2ZCwbf$(LJ9cn$ks(+bs#($p_EyWpfSZR4-hYBYmBlu^X=l4Dc~Z8kQDe4nPuEW=4zHBmwm zeakHQA=2aOebOf4oJ3J38-M7fngbhI3!{U;3oX5UwrQoj>3IBz0wI&{+fu`dGz08B zb8R#P{<>S#jB1yA4mKxgIMRO?#*a98XA|cAEFT~`X`?k$EhWS2AwF8bT_Sn;QZGq1 zt*D06j=bo7wf?bg6ZxI91r_JP&l#MX!|KYf^I@~wSy7aSm=r1rYAQ9PH`|XZRG)N| z*AO43ccfSIJPn+DPGx9wAkSJz4o;Z7QG8ln-t$boJocGbLamKo#fOMclWl$0Um;qi zCJJ1F5&-t2YeVxA5Z0Cf(ATG(<|z<;%7giIz!xiy.x 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 Sz%GFR0000/healthz` after the service starts. + +The default deployment pulls the upstream server image from GitHub Container Registry. Runtime parsing is local and deterministic for the included demo PDF. + +## Endpoints + +- `GET /healthz`: Checks that the upstream LiteParse server is reachable by sending `POST /parse` without a file and expecting the documented `400` response. +- `GET /demo`: Generates a small local PDF, sends it to `POST /parse?text=true` with OCR disabled, and verifies the expected text is returned. +- `GET /v1/models`: Returns an OpenAI-style metadata list for compatibility checks. It is not a hosted LLM endpoint. +- `GET /`: Returns a compact endpoint index. +- `POST /parse`: Proxies to upstream LiteParse. Upload a `file` form field and optionally a JSON `config` form field. Add `?text=true` for plain text output. +- `POST /screenshots`: Proxies to upstream LiteParse screenshot generation. Upload a `file` form field and optionally a JSON `config` form field. + +Example smoke checks to verify the deployed service: + +```bash +curl -fsS https:///healthz +curl -fsS https:///demo +curl -fsS https:///v1/models +``` + +Expected `/demo` fields include: + +```json +{ + "ok": true, + "demo": { + "contains_expected_text": true, + "credentials_required": false, + "hosted_model_call_attempted": false, + "model_weights_downloaded": false, + "ocr_enabled": false + } +} +``` + +Example parser call with your own PDF: + +```bash +curl -X POST "https:///parse?text=true" \ + -F "file=@document.pdf" \ + -F 'config={"ocrEnabled":false,"maxPages":5}' +``` + +Example screenshot call: + +```bash +curl -X POST "https:///screenshots?pages=1" \ + -F "file=@document.pdf" \ + -F 'config={"ocrEnabled":false}' +``` + +## Local Verification + +Use these commands from the parent monorepo worktree to verify the template metadata and compose file: + +```bash +python3 sdks/templates/validate.py +git -C sdks diff --check origin/main...HEAD +docker compose -f sdks/templates/prebuilt/liteparse/docker-compose.yml config >/dev/null +``` + +Optional local smoke run: + +```bash +docker compose -f sdks/templates/prebuilt/liteparse/docker-compose.yml up -d +curl -fsS http://localhost:8080/healthz +curl -fsS http://localhost:8080/demo +curl -fsS http://localhost:8080/v1/models +docker compose -f sdks/templates/prebuilt/liteparse/docker-compose.yml down +``` + +If local port `8080` is already in use, temporarily change only the host side of the proxy mapping, for example `18080:80`, then use `http://localhost:18080/healthz`. + +## Production Notes + +- This is a small no-secret parser deployment, not the full LiteParse production observability stack. +- The default image is the upstream slim server documented by LiteParse. The full upstream server example adds Redis caching, rate limiting, OpenTelemetry tracing, Jaeger, Prometheus, and Grafana; deploy those deliberately and size the CVM for them if you need that stack. +- Add authentication, authorization, content-length limits, file-type allowlists, request timeouts, logging, and abuse controls before accepting private or untrusted documents. +- The template disables OCR only for the generated `/demo` PDF. Real parser calls can enable OCR through LiteParse config, which increases CPU and memory use. +- Office document and image conversion depend on the upstream server image's included tooling. Test your real document formats and expected concurrency before production use. +- Pin `LITEPARSE_SERVER_IMAGE` to a digest or release tag for reproducibility. The default `main` tag follows upstream changes. +- The `/v1/models` endpoint is compatibility metadata only; no LLM model is hosted, downloaded, or called. + +## Security Notes + +- No credentials are required for the default template. +- Do not put API keys, bearer tokens, private keys, OTPs, or passwords in the compose file. +- The compose file does not use `env_file`, host bind mounts, privileged mode, host networking, host IPC, Docker socket access, GPU device reservations, or external build contexts. +- Caddy is the only public service. The upstream LiteParse server and verifier are reachable only on the internal Compose network. + +## Upstream Attribution + +This template uses the official LiteParse project and server wrapper: + +- LiteParse repository: https://github.com/run-llama/liteparse +- LiteParse docs: https://developers.llamaindex.ai/liteparse/ +- Library usage docs: https://developers.llamaindex.ai/liteparse/guides/library-usage/ +- Server usage docs: https://developers.llamaindex.ai/liteparse/guides/server-usage/ +- Server repository: https://github.com/run-llama/liteparse-server +- Server image documented by upstream: `ghcr.io/run-llama/liteparse-server:main` diff --git a/templates/prebuilt/liteparse/docker-compose.yml b/templates/prebuilt/liteparse/docker-compose.yml new file mode 100644 index 00000000..a52918bc --- /dev/null +++ b/templates/prebuilt/liteparse/docker-compose.yml @@ -0,0 +1,377 @@ +services: + liteparse: + image: ${LITEPARSE_SERVER_IMAGE:-ghcr.io/run-llama/liteparse-server:main} + restart: unless-stopped + init: true + environment: + NODE_ENV: production + NO_COLOR: "1" + expose: + - "5000" + networks: + - internal + + verifier: + image: python:3.12-slim-bookworm + restart: unless-stopped + init: true + environment: + APP_PORT: "8000" + LITEPARSE_SERVER_URL: http://liteparse:5000 + LITEPARSE_DEMO_TEXT: ${LITEPARSE_DEMO_TEXT:-LiteParse Phala Cloud demo} + PYTHONUNBUFFERED: "1" + expose: + - "8000" + configs: + - source: liteparse_verifier_app + target: /opt/liteparse-verifier/server.py + command: + - python + - /opt/liteparse-verifier/server.py + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=5).read()", + ] + interval: 30s + timeout: 10s + retries: 10 + start_period: 120s + depends_on: + liteparse: + condition: service_started + networks: + - internal + + proxy: + image: caddy:2.8 + restart: unless-stopped + ports: + - "8080:80" + configs: + - source: caddy_config + target: /etc/caddy/Caddyfile + depends_on: + verifier: + condition: service_healthy + networks: + - internal + +configs: + caddy_config: + content: | + :80 { + encode zstd gzip + header { + X-Content-Type-Options "nosniff" + Referrer-Policy "no-referrer" + -Server + } + + @liteparse_api { + path /parse /screenshots + } + handle @liteparse_api { + reverse_proxy liteparse:5000 + } + handle { + reverse_proxy verifier:8000 + } + } + + liteparse_verifier_app: + content: | + import json + import os + import platform + import sys + import time + import traceback + import urllib.error + import urllib.request + import uuid + from http import HTTPStatus + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + from urllib.parse import urlparse + + + STARTED_AT = time.time() + SERVICE = "liteparse-verifier" + UPSTREAM_REPO = "https://github.com/run-llama/liteparse" + SERVER_REPO = "https://github.com/run-llama/liteparse-server" + SERVER_URL = os.environ.get("LITEPARSE_SERVER_URL", "http://liteparse:5000").rstrip("/") + DEMO_TEXT = os.environ.get("LITEPARSE_DEMO_TEXT", "LiteParse Phala Cloud demo") + + + def json_response(handler, status, payload): + body = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8") + handler.send_response(status) + handler.send_header("Content-Type", "application/json; charset=utf-8") + handler.send_header("Cache-Control", "no-store") + handler.send_header("Content-Length", str(len(body))) + handler.end_headers() + handler.wfile.write(body) + + + def sample_pdf(text): + escaped = ( + text.replace("\\", "\\\\") + .replace("(", "\\(") + .replace(")", "\\)") + .encode("latin-1", "replace") + .decode("latin-1") + ) + stream = f"BT /F1 24 Tf 72 720 Td ({escaped}) Tj ET\n" + objects = [ + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n", + "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n", + "4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n", + f"5 0 obj\n<< /Length {len(stream.encode('latin-1'))} >>\nstream\n{stream}endstream\nendobj\n", + ] + pdf = "%PDF-1.4\n" + offsets = [0] + for obj in objects: + offsets.append(len(pdf.encode("latin-1"))) + pdf += obj + xref_offset = len(pdf.encode("latin-1")) + pdf += f"xref\n0 {len(objects) + 1}\n0000000000 65535 f \n" + for offset in offsets[1:]: + pdf += f"{offset:010d} 00000 n \n" + pdf += f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n" + return pdf.encode("latin-1") + + + def multipart_body(fields, files): + boundary = f"----phala-liteparse-{uuid.uuid4().hex}" + chunks = [] + for name, value in fields.items(): + chunks.append(f"--{boundary}\r\n".encode("utf-8")) + chunks.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode("utf-8")) + chunks.append(value.encode("utf-8")) + chunks.append(b"\r\n") + for name, filename, content_type, data in files: + chunks.append(f"--{boundary}\r\n".encode("utf-8")) + chunks.append( + ( + f'Content-Disposition: form-data; name="{name}"; ' + f'filename="{filename}"\r\n' + f"Content-Type: {content_type}\r\n\r\n" + ).encode("utf-8") + ) + chunks.append(data) + chunks.append(b"\r\n") + chunks.append(f"--{boundary}--\r\n".encode("utf-8")) + return boundary, b"".join(chunks) + + + def upstream_request(path, method="GET", body=None, headers=None, timeout=8): + request = urllib.request.Request( + f"{SERVER_URL}{path}", + data=body, + headers=headers or {}, + method=method, + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + return { + "ok": 200 <= response.status < 300, + "status": response.status, + "headers": dict(response.headers.items()), + "body": response.read(), + "error": None, + } + except urllib.error.HTTPError as exc: + return { + "ok": False, + "status": exc.code, + "headers": dict(exc.headers.items()), + "body": exc.read(), + "error": None, + } + except Exception as exc: + return { + "ok": False, + "status": None, + "headers": {}, + "body": b"", + "error": f"{type(exc).__name__}: {exc}", + } + + + def upstream_health(): + response = upstream_request("/parse", method="POST", body=b"", timeout=5) + healthy = response["status"] == 400 and not response["error"] + body_text = response["body"].decode("utf-8", "replace")[:500] + return { + "ok": healthy, + "server_url": SERVER_URL, + "probe": "POST /parse without a file should return HTTP 400", + "status": response["status"], + "error": response["error"], + "body_preview": body_text, + } + + + def run_demo(): + pdf = sample_pdf(DEMO_TEXT) + config = { + "ocrEnabled": False, + "maxPages": 1, + } + boundary, body = multipart_body( + {"config": json.dumps(config, separators=(",", ":"))}, + [("file", "liteparse-phala-demo.pdf", "application/pdf", pdf)], + ) + response = upstream_request( + "/parse?text=true", + method="POST", + body=body, + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + timeout=30, + ) + text = response["body"].decode("utf-8", "replace").strip() + return { + "ok": response["status"] == 200 and DEMO_TEXT in text, + "upstream_status": response["status"], + "upstream_error": response["error"], + "input": { + "filename": "liteparse-phala-demo.pdf", + "bytes": len(pdf), + "config": config, + }, + "parsed_text": text, + "contains_expected_text": DEMO_TEXT in text, + "credentials_required": False, + "hosted_model_call_attempted": False, + "model_weights_downloaded": False, + "ocr_enabled": False, + } + + + def base_payload(): + return { + "service": SERVICE, + "upstream_repo": UPSTREAM_REPO, + "server_repo": SERVER_REPO, + "server_url": SERVER_URL, + "python": sys.version.split()[0], + "platform": platform.platform(), + "uptime_seconds": round(time.time() - STARTED_AT, 3), + "endpoints": ["/healthz", "/demo", "/v1/models", "/parse", "/screenshots"], + } + + + class Handler(BaseHTTPRequestHandler): + server_version = "liteparse-verifier/1.0" + + def log_message(self, fmt, *args): + print( + json.dumps( + { + "ts": time.time(), + "client": self.client_address[0], + "request": self.requestline, + "message": fmt % args, + } + ), + flush=True, + ) + + def do_GET(self): + path = urlparse(self.path).path.rstrip("/") or "/" + + if path == "/healthz": + health = upstream_health() + payload = base_payload() + payload.update( + { + "ok": health["ok"], + "status": "ok" if health["ok"] else "unhealthy", + "liteparse_server": health, + } + ) + json_response(self, HTTPStatus.OK if health["ok"] else HTTPStatus.SERVICE_UNAVAILABLE, payload) + return + + if path == "/demo": + payload = base_payload() + try: + demo = run_demo() + payload.update({"ok": demo["ok"], "status": "ok" if demo["ok"] else "failed", "demo": demo}) + json_response(self, HTTPStatus.OK if demo["ok"] else HTTPStatus.BAD_GATEWAY, payload) + except Exception as exc: + payload.update( + { + "ok": False, + "status": "failed", + "error": f"{type(exc).__name__}: {exc}", + "traceback": traceback.format_exc().splitlines()[-6:], + } + ) + json_response(self, HTTPStatus.INTERNAL_SERVER_ERROR, payload) + return + + if path == "/v1/models": + json_response( + self, + HTTPStatus.OK, + { + "object": "list", + "data": [ + { + "id": "liteparse/local-document-parser", + "object": "model", + "created": 0, + "owned_by": "run-llama", + "description": ( + "Metadata endpoint for the local LiteParse document parser. " + "No hosted LLM model is loaded or called by this template." + ), + } + ], + }, + ) + return + + if path == "/": + payload = base_payload() + payload.update( + { + "ok": True, + "description": "LiteParse HTTP smoke template. Use POST /parse and POST /screenshots for parser calls.", + } + ) + json_response(self, HTTPStatus.OK, payload) + return + + json_response( + self, + HTTPStatus.NOT_FOUND, + {"ok": False, "error": "not_found", "endpoints": base_payload()["endpoints"]}, + ) + + + if __name__ == "__main__": + port = int(os.environ.get("APP_PORT", "8000")) + print( + json.dumps( + { + "event": "startup", + "service": SERVICE, + "port": port, + "upstream_repo": UPSTREAM_REPO, + "server_url": SERVER_URL, + "credentials_required": False, + } + ), + flush=True, + ) + ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever() + +networks: + internal: + driver: bridge