From dd8da386f46dbcbed26d043a85b88b2133a11d09 Mon Sep 17 00:00:00 2001 From: wh Date: Fri, 10 Apr 2026 15:27:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(US1):=20text=20triple=20extraction=20?= =?UTF-8?q?=E2=80=94=20POST=20/api/v1/text/extract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/models/text_models.py: TripleItem, SourceOffset, TextExtract{Request,Response} - app/services/text_service.py: TXT/PDF/DOCX parsing + LLM call + JSON parse - app/routers/text.py: POST /text/extract handler with Depends injection - tests/test_text_service.py: 6 unit tests (formats, errors) - tests/test_text_router.py: 4 router tests (200, 400, 502×2) - 10/10 tests passing --- app/__pycache__/main.cpython-312.pyc | Bin 0 -> 2100 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 155 bytes .../__pycache__/text_models.cpython-312.pyc | Bin 0 -> 1300 bytes app/models/text_models.py | 25 ++++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 156 bytes .../__pycache__/finetune.cpython-312.pyc | Bin 0 -> 258 bytes app/routers/__pycache__/image.cpython-312.pyc | Bin 0 -> 252 bytes app/routers/__pycache__/qa.cpython-312.pyc | Bin 0 -> 246 bytes app/routers/__pycache__/text.cpython-312.pyc | Bin 0 -> 1126 bytes app/routers/__pycache__/video.cpython-312.pyc | Bin 0 -> 252 bytes app/routers/text.py | 17 ++- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 157 bytes .../__pycache__/text_service.cpython-312.pyc | Bin 0 -> 4787 bytes app/services/text_service.py | 95 ++++++++++++++ ...t_text_router.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 7732 bytes ..._text_service.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 12889 bytes tests/test_text_router.py | 63 +++++++++ tests/test_text_service.py | 122 ++++++++++++++++++ 18 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 app/__pycache__/main.cpython-312.pyc create mode 100644 app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 app/models/__pycache__/text_models.cpython-312.pyc create mode 100644 app/models/text_models.py create mode 100644 app/routers/__pycache__/__init__.cpython-312.pyc create mode 100644 app/routers/__pycache__/finetune.cpython-312.pyc create mode 100644 app/routers/__pycache__/image.cpython-312.pyc create mode 100644 app/routers/__pycache__/qa.cpython-312.pyc create mode 100644 app/routers/__pycache__/text.cpython-312.pyc create mode 100644 app/routers/__pycache__/video.cpython-312.pyc create mode 100644 app/services/__pycache__/__init__.cpython-312.pyc create mode 100644 app/services/__pycache__/text_service.cpython-312.pyc create mode 100644 app/services/text_service.py create mode 100644 tests/__pycache__/test_text_router.cpython-312-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_text_service.cpython-312-pytest-9.0.3.pyc create mode 100644 tests/test_text_router.py create mode 100644 tests/test_text_service.py diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22e3cc51d943f8851cd9e7f32bc3697a914f8c47 GIT binary patch literal 2100 zcmbVN-A@!(6u);qcNbWJ6=4guXlvqHyNIbqgw&#FHCWQrr%p&VbbMaf;Gm(N#@)$=lsq& z_nwbCKiAd85In{=%hq8Pq2I*E#-ml3bwfg^hzKIsMh-SG7QAFjj%>&wFWZWv8mgli znxh+fC{yf+6E&hv%!pwmAsVAK3%KmjsHK>wR34Iuc12mJgfZ#>B@>-QNR-ws#uk(c zI!@ywuT(%=78>2~hY=ulZBbQNs#0wM_AZ`hVj8Ia1ZGbezm<|2Qk%nzbsIPmzk;f} zPCzcU+1s5aqsiG}>~NZmW~aqyags(djCp9Qr!r5QMh-tMCH0F98xfvqx#NW`n_t+n z<%KPi&$k?JOeOvh$w*ubC2w07o zjPh8H1{oW~;6yg_`D~?e!=k~MAQ`%9eFB(%XsjX%LWLkBa+R$z1QUtK7G6{~%S+HT zyssD!p;MwCluV|B}*o zPl?{EC}MM6rSyr??!L+XJIcv_d~p|i*wu|E>d%VE?QqOWBNX<6>fzVlu73aV%9lT_ z-uU6c+|+}c^D95zT>1Qlz^z`t`tW-3!B?NHe0p_4?Mxp?A4n-&4XnVXyoOLe%dD_( zxIRjm4-FJC8%=uY}SAE3o@)5l2jX=uHAj)mwbLAq*16+eT z1O=Z<-i7ZFYZG925fc_%E5~3j!^duT{RSXmitg8LpC+Z6){@d%IlijnlnQ%*T5{EL zpxj&?wFvdNbP;MhWVtjbxD+;S=B~fJvC3T8)_EqHlez|>bUqD`PFaSi2zNs%Iiw>{TEVoP2XajAQMgP?+3ajV~l@6`&LlvV@1KzBLw7e4B_MW*Jxt; z^zG=*No`F-Qq!6w<5PH6{}YLIU9O5A)gnE<9BW&yKe}9-SgvdOE2`?!q`VeKSh_I|p@<2{`wUX^%gV(nraZqWySN}R zIW;CHF)1|%LdGX%#uuj+m1P2j6AKDra`RJCbBbf)<1_OzOXB183My}L*Z>7fb5iY! WSb;_{0&y{j@sXL4k+Fyw$N~U5%_oTf literal 0 HcmV?d00001 diff --git a/app/models/__pycache__/text_models.cpython-312.pyc b/app/models/__pycache__/text_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c24b9e0d72ced8e8f9cc2a210a8972fd620dd6bd GIT binary patch literal 1300 zcmai!&ubGw6vt<>yZP1psBNJwjjbqD_u@qlTB?Xx)T$}idI^DHo1N0t&F*$)qK&6Q zD(Eeq+JC@<_%HY$c#`(840!V7O=!G$^37~2X$W*7Z{B|2&YO9kd6RF&VjjV>{VDdo zVuXIGm($1UJ15)V93U6D*hfuV!x*K|L*%CRk!w)=#Yn!ZrL<~-Y7MA{R@0zn22@k4 zSx|EWs@2ZhW4}~78_V2%htp@FOZ`T&Kw|r=p_9w-et;;dVHed>FAz1T>EdNjwU<&} z+ptYpSPxsQLD#mnI2F?3!eK%hG;pi7C9{MCPLmQM^Mo`*x8*CpK*&zZ@%s_O3xq5a z;sik`9N~olC*(6aQ5*Vu=fP6_O~_vH$Z62J?`%>(vBdERr|h){vJ*vhNKSoT7j##s zg}}cawV8sfjR_7ffO&`Fu~KI>&K33_vC?UHG=qB(K97N>Vf^P}YR>{zmL`<8Gcv!y zyvV1I1#QXXBs}n<2v`>T@1aIpwbLxC_M1~i^_4WZ zU`!R2)LB%XE~%`%3#Ud+;1|I_?ZxrV)3`j*sm6tJ=Se&@ePm9@lMBb@LNZfL8VzF= zBIm(yjhaOb*La(OHZffd1GMyjmS8dLw4B(0vs&2|%xQ?{bf-l*P}%aJLn23P_g&Q^ zRcYxHmF0+q%}5Xc8TmlY&I}P@)3C{eNJV+7Xf}jpR%yzpQu#G7fJGD0W~Q>Y^8QA5 z>ci5JS&1(%?yYs#4{v^~9h-{@AvJY0p?Qc56H;GOE4euBlx@h#Ggb2lx}8ePgOd1& z6E=Mh&RCzMKIfrY79cxQW(Iy-1yy^OyZP^9bI0afvSAg6)G`~jT_+G;gQ-iNw2aQI ziRe^k)648~a%JzbD=-88z!1L$=C^?{{(_I|p@<2{`wUX^%i6^%raZqWySN}R zIW;CHF)1|%LdGX%#uuj+m1P2j6AKDritQ`l2&{A^!)K88U)C;GG3EJ1*~JBk$*D0piAkwB5Hdb7Grl;rs4NpGoLEp0 z1JPC-lLoaxub}c4hfQvNN@-52T@g3XERf@hMS#QyW=2NFy9}}qIfZ6O&EUDhsoKa^ I#0iuD0DtmD%>V!Z literal 0 HcmV?d00001 diff --git a/app/routers/__pycache__/image.cpython-312.pyc b/app/routers/__pycache__/image.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c78dda6ccfdc3e7d7aac2c62b6f6125e092e49a GIT binary patch literal 252 zcmX@j%ge<81nG-!WI6%q#~=<2Fhd!iRe+4?3@HpLj5!Rsj8Tk?3``8Ej44d%jBA)z zGeT5FF;%i?vc3dyG#PJkItF+K<(HPE7Tsd?%uP&B1&XnhB&HYpX)@npPfIK=Ni4`L z0$Fg2tq81cCBtWs-d`3jRx#!IMcKs#iOH!kIf+TBIS?{FF*Ck6wWur;D4bYO5ChRx z9Fqw$Uaz3?7Kcr4eoARhs$CH`&>)briba6L2WCb_#=8u%4>^Tqh|l1>!l~5AR>TRE F004KKLizvz literal 0 HcmV?d00001 diff --git a/app/routers/__pycache__/qa.cpython-312.pyc b/app/routers/__pycache__/qa.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d735a8c0f604ad1ff6a6be754bf5a6276d82b19b GIT binary patch literal 246 zcmX@j%ge<81o?|^WZDDi#~=<2Fhd!iRe+4?3@HpLj5!Rsj8Tk?3``8Ej44d%jBA)z zGeT5FF;%i?vc3dyG#PJkItF+K<(HPE7TsbBbOZ{plq9AX`)M-YVoysfE=erNECQK+ zi>(N(XeGmEkj7u8E>*Vi^C>2KczG$)vkyeXbQ+d#UeoB12ZEd<6Q>Xhnzw)L}svE;goM=E8+x7005vm BKyCm4 literal 0 HcmV?d00001 diff --git a/app/routers/__pycache__/text.cpython-312.pyc b/app/routers/__pycache__/text.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cc5fcbd6d6580a0cb82035e8c08c19d5683468b GIT binary patch literal 1126 zcmY*Y&1(}u6rb6z>^7UEiMA-AQnd;Zn}QxHB9dx7s8vKgEy1v6ry+GWS!XudY7f&70rbeUnOcAy^NdeXKv0 z5c(>N&I%)7tiJ^@haBW!7kSvio@7Z_*pe%Iilv0Q?5dt-X`!yT2~W3l&#;WpR^2Wy zX(c_=GDBN)Q(oFiVI*o!*2<8~#Li9&e^19C=3>_Z^e7hONn|r8w2V%L zamh)Nq+=4rNfFIS6LVa5GI!L4C>2@RsqRAdhp-9~H%88#Jl|{ug!05F@kzsB1Um1+p#IbpL16#JlZ?jo5CPD}>Wt_J7H)fQm^ z-}0Z2`OO9+aDy2Dw899zR<9DS2oQLcz^<_ysUZ;@f-MSxJ55QHSn-+;ak)&%RW3t2 zJQ4T6HA;dOZB*s>$>LC9&%$0G192K*m}n4lj?hHRD}*-JG@8cK(n4Hg7Uq2u^heU1 zd{X%ZQn1`a5k(gNGSSK+}ylED7{e56m@rl_>O+hpkF4+?f8%# ze3u=3UVU}&bz(VN`j9LwDW#=kX@d#y?fd|Kf_EuT)d6Td#}nf=3v9nmMRQaE!MDQI z#b~;$2-FrY+b|tLA>a0IC?-4#Ya-kehMSZWo$zZSjcT1Rnh>RR0q7NgaB{2|jy=+; zArRu1KAAou!#1E4E+ccX}RPm6@QMHDw_q-mNCYwXzw?) zZv`D#MWd_e)C$`3#mLR~&g<`uoikcnLy~?ke=C2#vNTwl$uHxPH4XLl&0kvX+5f3; zU}?|ra^In~go;P-M)briba6L2WCb_#=8u%4>^Tqh|l1>!l~5AR>TRE F004|^Lni TextExtractResponse: + return await text_service.extract_triples(req, llm, storage) diff --git a/app/services/__pycache__/__init__.cpython-312.pyc b/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f297b10abfb8a2b0862a5c4b6efaae97e0136c8b GIT binary patch literal 157 zcmX@j%ge<81UDAl$OO@kK?FMZ%mNgd&QQsq$>_I|p@<2{`wUX^%f`hjraZqWySN}R zIW;CHF)1|%LdGX%#uuj+m1P2j6AKDrpzPw9`1s7c%#!$cy@JYH95%W6DWy57c15f} U!x(|M7{vI<%*e=C#0+Es0G_WXYXATM literal 0 HcmV?d00001 diff --git a/app/services/__pycache__/text_service.cpython-312.pyc b/app/services/__pycache__/text_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2a5aa7554ad826a41983f86d918a38a080b67f4 GIT binary patch literal 4787 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 literal 0 HcmV?d00001 diff --git a/app/services/text_service.py b/app/services/text_service.py new file mode 100644 index 0000000..18c2003 --- /dev/null +++ b/app/services/text_service.py @@ -0,0 +1,95 @@ +import io + +import pdfplumber +import docx + +from app.clients.llm.base import LLMClient +from app.clients.storage.base import StorageClient +from app.core.config import get_config +from app.core.exceptions import UnsupportedFileTypeError +from app.core.json_utils import extract_json +from app.core.logging import get_logger +from app.models.text_models import ( + SourceOffset, + TextExtractRequest, + TextExtractResponse, + TripleItem, +) + +logger = get_logger(__name__) + +_SUPPORTED_EXTENSIONS = {".txt", ".pdf", ".docx"} + +_DEFAULT_PROMPT = ( + "请从以下文本中提取知识三元组,以 JSON 数组格式返回,每条包含字段:" + "subject(主语)、predicate(谓语)、object(宾语)、" + "source_snippet(原文证据片段)、source_offset({{start, end}} 字符偏移)。\n\n" + "文本内容:\n{text}" +) + + +def _file_extension(file_name: str) -> str: + idx = file_name.rfind(".") + return file_name[idx:].lower() if idx != -1 else "" + + +def _parse_txt(data: bytes) -> str: + return data.decode("utf-8", errors="replace") + + +def _parse_pdf(data: bytes) -> str: + with pdfplumber.open(io.BytesIO(data)) as pdf: + pages = [page.extract_text() or "" for page in pdf.pages] + return "\n".join(pages) + + +def _parse_docx(data: bytes) -> str: + doc = docx.Document(io.BytesIO(data)) + return "\n".join(p.text for p in doc.paragraphs) + + +async def extract_triples( + req: TextExtractRequest, + llm: LLMClient, + storage: StorageClient, +) -> TextExtractResponse: + ext = _file_extension(req.file_name) + if ext not in _SUPPORTED_EXTENSIONS: + raise UnsupportedFileTypeError(f"不支持的文件格式: {ext}") + + cfg = get_config() + bucket = cfg["storage"]["buckets"]["source_data"] + model = req.model or cfg["models"]["default_text"] + + data = await storage.download_bytes(bucket, req.file_path) + + if ext == ".txt": + text = _parse_txt(data) + elif ext == ".pdf": + text = _parse_pdf(data) + else: + text = _parse_docx(data) + + prompt_template = req.prompt_template or _DEFAULT_PROMPT + prompt = prompt_template.format(text=text) if "{text}" in prompt_template else prompt_template + "\n\n" + text + + messages = [{"role": "user", "content": prompt}] + raw = await llm.chat(model, messages) + + logger.info("text_extract", extra={"file": req.file_name, "model": model}) + + items_raw = extract_json(raw) + items = [ + TripleItem( + subject=item["subject"], + predicate=item["predicate"], + object=item["object"], + source_snippet=item["source_snippet"], + source_offset=SourceOffset( + start=item["source_offset"]["start"], + end=item["source_offset"]["end"], + ), + ) + for item in items_raw + ] + return TextExtractResponse(items=items) diff --git a/tests/__pycache__/test_text_router.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_text_router.cpython-312-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beb93f267eb97e29a07267b943a0d894144e02bd GIT binary patch literal 7732 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_text_service.cpython-312-pytest-9.0.3.pyc b/tests/__pycache__/test_text_service.cpython-312-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..406e9eac0c92a2c3273814b6c4c5dd113ced1b5e GIT binary patch literal 12889 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