From 19ca9d06ce378f9ae976d872682edf33dd6fcef6 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Fri, 11 Jul 2025 13:50:08 +0800 Subject: [PATCH] =?UTF-8?q?=E9=93=BE=E6=8E=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E4=BB=A5=E8=BF=90=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/__pycache__/dependencies.cpython-312.pyc | Bin 0 -> 1594 bytes api/__pycache__/main.cpython-312.pyc | Bin 3961 -> 2705 bytes api/dependencies.py | 45 ++ api/main.py | 52 +- api/models/__pycache__/poster.cpython-312.pyc | Bin 0 -> 5057 bytes api/models/__pycache__/prompt.cpython-312.pyc | Bin 0 -> 7343 bytes api/models/prompt.py | 186 ++++++ .../__pycache__/poster.cpython-312.pyc | Bin 0 -> 4772 bytes .../__pycache__/prompt.cpython-312.pyc | Bin 0 -> 10282 bytes api/routers/__pycache__/tweet.cpython-312.pyc | Bin 6202 -> 6210 bytes api/routers/poster.py | 4 +- api/routers/prompt.py | 207 ++++++ api/routers/tweet.py | 4 +- .../__pycache__/poster.cpython-312.pyc | Bin 0 -> 5368 bytes .../prompt_builder.cpython-312.pyc | Bin 0 -> 7271 bytes .../prompt_service.cpython-312.pyc | Bin 0 -> 28523 bytes api/services/prompt_builder.py | 191 ++++++ api/services/prompt_service.py | 619 ++++++++++++++++++ 18 files changed, 1258 insertions(+), 50 deletions(-) create mode 100644 api/__pycache__/dependencies.cpython-312.pyc create mode 100644 api/dependencies.py create mode 100644 api/models/__pycache__/poster.cpython-312.pyc create mode 100644 api/models/__pycache__/prompt.cpython-312.pyc create mode 100644 api/models/prompt.py create mode 100644 api/routers/__pycache__/poster.cpython-312.pyc create mode 100644 api/routers/__pycache__/prompt.cpython-312.pyc create mode 100644 api/routers/prompt.py create mode 100644 api/services/__pycache__/poster.cpython-312.pyc create mode 100644 api/services/__pycache__/prompt_builder.cpython-312.pyc create mode 100644 api/services/__pycache__/prompt_service.cpython-312.pyc create mode 100644 api/services/prompt_builder.py create mode 100644 api/services/prompt_service.py diff --git a/api/__pycache__/dependencies.cpython-312.pyc b/api/__pycache__/dependencies.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17cdd93734d4cdfcaf2e847b028deca41514eb3b GIT binary patch literal 1594 zcmb7EU1%d!6u$G5%p~a~*(T}Qic77g>ujO*!3U9bA*@IPn^ag4%*!xjCUMG4CfrQa zEGdk*Rl&c=LaRt&7ZD?cMUh2N+&7bf0Xt-M4C16rVizPG-~X+g`Zmes|8f z_dEBTFZoT9q6o&X@7CXF^9cPxHo;(rU{C?DjxWPJ?d}iV{_59vyEpn9>(P58A({2YtGc6YenQ}lbr2;u+jhK!~REW{nc*&d)M!P7xpWS2|E;1)Ko`z z3`_TTwQeX^r0nZiE2}Bl#cUya*$ZR6(KKn*bNPa;>89TpPgD)XBKx8&VN!7~exijb z(^fU5f^AFD46K(O8@Dj2pvTqhw>+`o;0hteVS3u8O?@DMSlPz<{4m!s5)mEA1e3WU&Oi!sV%_k7c{-D z*EGFWHuT1Pz2%)YYKCK|rg24Aj{EQ=+;|FRV;PK#?nlwYGrf%L7Weq^9e#G3pS}L} z@BFiWhf!!|H<9Wru3!4>(zW7FdTu*C*InwR&u^t(+>$Q1FYje$u0P*>t(SSuEt2qa zd&x69NqIXdch7xi^^z~S`MtzMr`1b5`Oe}1*bwQ7)5NuhbWb!(bsq-+x+j5jT!to7 zH5`8OWB=2&d#fKG{BrZ)=8c2Sd;KrEGK0y1ZxNn?1wTeiIxtTG7>Hn8v>%(?;wNd) zBhZqoecD`p|L3oO68MNR@e{;6p!CK_YoTAXn|K-Z`ueIHxjABHLIbM0M>{+CgIR5s@9-J zloQD~j3Zbl(fMc<)zC2#E9|U+)(N8E*f_-Xct!}m=NqcZn%}QKZZ+6(BW@>*gq<{! z5b!kJ>*8xFi5GI4tvG>`JY(bEXZ<5OQOM0~5xxyK{a1K$OY#&>Z;I~Sf+gd3^ep$_ z=2`Hkktq_vEmt}Lpc((q(?*)NHeKl7BE@#R1GnP(`L^HfhTyt%UCOqz$I_q%y}$bL~KII#+Bcb!O7)k@)XRf?1^%ZjjN{zkf&PC_&A{+GK*sl6*;|aX3=!b zyk$<4i|Z@OLMq0;EC!985A4syxFTk;e1abz!S|css}7XVosnB3uVUScD7S=SuTkd$ z>Rd!!ODrDjLqk17y=*u*0b}fWaJVBYdx|?ni0i2(r|j}N)Dm^hS1fXv4se_3W%)QiS7Zp` z=b#MHss> V=w4CTTfLwC)DN;#(OpRJ{sP1z?u!5b literal 3961 zcmbUjYj6|Cc~5uJ>0!yX3&=k5g9 zlgtQW2bz+^OexM3Q=kk}CFYSn5)3J!|NF4&^0fY#Jv$Og`Ki5UK%N-uuCqBmqp4b z?3T;p{)j&why)m9Ln70^-a1rq2JD97%7U;5mt6Nwu;x0?y~)}w&SLchW3D1K*e2A% zzYhNOq7(Z>w#SA`hq;M7d87f(z~VAoj{Rc8^}qy|=WG-k`*#*)T9D|#6}=3uq$5Dr zt0wH`9^9^a7)4g&8W@{!EsSeG#%_zVIE()NJ;jv;;W}Io@*0Zw`x@@RbGaMws{i8d zv}6}&yxOu2ZW=}fD+EE;^QhMpsMkJ0-6B@wb+3cplzJL9t&vu+Vr%~^MLiaTn{g1g z;L7W*mSq_DussWwOcZdUG0;?fL_qBKb;Lt${V_7^VC?a42f;yOp z#fbFdT3kp7y`lol{$5dw0=7r$jpnhscUR}ZB&J6JH1P7fI(PNLp#b!Dr?i2Tb}%o1 z(VbY(L`{l|Ak3~E6GcsT3?x-eRED+&*9&Tl?x{_in}X{T0+5-YEW%x%SjRUK)6J9?5zT+no$DD8=BIx7qe#PShN?Mz~pg+yNY z2(bGR0~xK+4KnoS^QC|Q05g_f8KmXo7ZsvGU^RzD-S4uX;*4!YZp>;>Tgm z6=D6QMVtj;&XRKhU9`Q)Xyt_+`y0#zT!?&v>xt7;nz$!N?9i&LwZ-$MO{=wVZxl`| zc)!6aymcPPW#55f2c5YaR|%;lrI2vpa9DSknW1ySfD|3_ z29E^eK^zV43myy}(jAJZresZb3Q{yq@+DOR;tKo)iN?({xznRvbT_Yzo~S?xno|C! zSa1Kvpd=xm?vRtckg7T-B_Q}ov(kOf^sh8}>_>Lbsr@JRPnB(c zz=h{3TW2am>8?4>Kg+dDb1j#TKj5}3IZ#=}xqV{?&K~$_*KEbc>57e+?wN`mQ-PgR z-j~vQ<|@}--j;cFrgCe#>#+lsubwRnO_zl-8-5p`DeFjg&Xrb;4$hRWN$;EI+@`V( z54eqUoC~Ne^#oJcZvHbD{7NMy;ADGwdw?0LdEPBal}@bzqTJX>ZgSFid&IbTXYtfW z5TM5C3-kBJ3$7lre=EEtyd}gzt&}ub)IC^KV~RAONy&uH9~Bi90BRqd*>D)>yn-wz zk42M;)GH-az1&>J6cK}SBtcd!inyrtC2?WdCkhy(#o$zhj}%Jcv=tu=PZq{Q=tHI} z#3-ave27+HL<;ebvdS9155Niuk2IR|)J}1=N*gTxSLw=TAO`uXVN$|mAly`iUP9sz zf{I3)@&e!w!(SzlPILv*-T#-WMxx*uRkYbX9sbHzp!*@AB|VsG8gkR9rM*lcbsRdI zV?Ysmq~kiL3BBr&hZ2}wPVoYT&a^#c3R17%g8vFhYqJZH*MYwxt;Ipf*3cBJ$`*R+t@UF*k0@8^lfvpmE}warbuaYC3o6+QP)G zg_C!5-c&u<1$Brsj4k=t2TYyF|}&jGS5}Im+gFsV+rwgN8UmW ztKbn7g>ot-qS2ueD0pE4Z-Y}DjYAo>cX+4qAJ7@x*JL|dDzL)YYM4Wx(D_| zARrGr+BIlx*LjK-B(unSQ<@~J;T}mAqf(N#2eWT@$SoAe6sRCH12}uJJECKENG%pOWmgPDe!nwN$4yzrf1#k+yMoiSU@j6MD-6*>qFGIfP#<+OhuZXMS*D)$hynl-}K{6SRDIaw5#reZ8NUsv;$cDRcT+=Q<`>W%PZ5qFPMrYTOHH#0sjcWw8A1= zB}FW6N4ziVZqAl{J6jUSdMh8h?7S__E|npMn??R<cY@lj1n60WaXR=uGASUn_ zIT+g`Kf}~LaxhE<8S0jt1;)|ujO{qPBU3WDck;z43;y}2M*8x diff --git a/api/dependencies.py b/api/dependencies.py new file mode 100644 index 0000000..7d14fdf --- /dev/null +++ b/api/dependencies.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +API依赖注入模块 +""" + +from core.config import get_config_manager, ConfigManager +from core.ai import AIAgent +from utils.file_io import OutputManager + +# 全局依赖 +config_manager = None +ai_agent = None +output_manager = None + +def initialize_dependencies(): + """初始化全局依赖""" + global config_manager, ai_agent, output_manager + + # 初始化配置 + config_manager = get_config_manager() + config_manager.load_from_directory("config") + + # 初始化输出管理器 + from datetime import datetime + run_id = f"api_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + output_manager = OutputManager("result", run_id) + + # 初始化AI代理 + from core.config import AIModelConfig + ai_config = config_manager.get_config('ai_model', AIModelConfig) + ai_agent = AIAgent(ai_config) + +def get_config() -> ConfigManager: + """获取配置管理器""" + return config_manager + +def get_ai_agent() -> AIAgent: + """获取AI代理""" + return ai_agent + +def get_output_manager() -> OutputManager: + """获取输出管理器""" + return output_manager \ No newline at end of file diff --git a/api/main.py b/api/main.py index 27683a3..40209ed 100644 --- a/api/main.py +++ b/api/main.py @@ -7,16 +7,11 @@ TravelContentCreator API服务 """ import logging -from fastapi import FastAPI, Depends +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager -from core.config import get_config_manager, AIModelConfig -from core.ai import AIAgent -from utils.file_io import OutputManager -from datetime import datetime - -from api.routers import tweet, poster +from api import dependencies # 配置日志 logging.basicConfig( @@ -26,32 +21,15 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -# 全局依赖 -config_manager = None -ai_agent = None -output_manager = None - @asynccontextmanager async def lifespan(app: FastAPI): """ 应用生命周期管理 在应用启动时初始化全局依赖,在应用关闭时清理资源 """ - global config_manager, ai_agent, output_manager - # 初始化配置 logger.info("正在初始化API服务...") - config_manager = get_config_manager() - config_manager.load_from_directory("config") - - # 初始化输出管理器 - run_id = f"api_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - output_manager = OutputManager("result", run_id) - - # 初始化AI代理 - ai_config = config_manager.get_config('ai_model', AIModelConfig) - ai_agent = AIAgent(ai_config) - + dependencies.initialize_dependencies() logger.info("API服务初始化完成") yield @@ -77,37 +55,19 @@ app.add_middleware( allow_headers=["*"], ) -# 依赖注入函数 -def get_config(): - return config_manager - -def get_ai_agent(): - return ai_agent - -def get_output_manager(): - return output_manager +# 导入路由 +from api.routers import tweet, poster, prompt # 包含路由 app.include_router(tweet.router, prefix="/api/tweet", tags=["tweet"]) app.include_router(poster.router, prefix="/api/poster", tags=["poster"]) +app.include_router(prompt.router, prefix="/api/prompt", tags=["prompt"]) @app.get("/") async def root(): """API根路径,返回简单的欢迎信息""" return {"message": "欢迎使用TravelContentCreator API服务"} -@app.get("/health") -async def health_check(): - """健康检查端点""" - return { - "status": "healthy", - "services": { - "ai_agent": ai_agent is not None, - "config": config_manager is not None, - "output_manager": output_manager is not None - } - } - if __name__ == "__main__": import uvicorn uvicorn.run("api.main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/api/models/__pycache__/poster.cpython-312.pyc b/api/models/__pycache__/poster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..639d214048f2b538aef721859902f4e9c719e792 GIT binary patch literal 5057 zcmb7HU2GFq79QJUd;F6GLMUWOAQ0M(S&@XFQu;&FCS`$^hC)GAtyM=RGk{nA>-6xwFYfon+eA(0eg`1azTgJVPee87DixO7{MxXmn?KHuaorJpx2($>mt1z z^g42S-K5tEy{??z0@CY--h!Oo!sbFxk-7+0aT-6}FpmEC=6lF02EQ3U zTaKR-yc~i(JIV=;oKctP7VViCoYV)x>w+-2vnv%ebDjdN;JvV{h|(_cV^x$Dbup|) zLy|su#%PO8T)koZ<#QUFYW24;svl|92W}dt291t^Uv3}QkGJZvOGe+9ljq|1qKAzu z-y2_lnZD9~(WJQqQErm_Bw(7oDI8M7kfJV!IZ}yR;GVvAPQQH$=18CHPYrd#bE#X` z^{!z()}gtSaKzul`$K|wuu)xuyXrq{e0vAR(iaAC-SeLt-}R;vLyh5(sJERq`fkF8 zCR;nw!vmAicH`JZtw0fjk$_JT`H(Lt=6#iHvR1*&GhYQ0Rg3!ff}VG5vUVYUr|Y3IcWMUbJiS@E!ej zG&MA23=gDxTGAKd`qd*7H?F3K?!b$@T>Ct9!sz4|!>L3^`pl1$hrjye_F;7czBHKw zyi&vXuBpTo{hMKtjYeH6hNtnCF)Le` zyY7W|%lD5h`#@W==YgFqE57e!Dk{g8uX<3#EiSxoaV~bYyQO8|D`5(H3Z>s*dkLB{ zFq)m`g>VzkOAhS9Hv6jppl0Wx@IBOqaT$-L51Y>{Ouu;rRI z_(kNMU|N}RcE$M8%D#q?7e7ia<)d5LcE>7Xa;&QTqfwS0UsM)5rcpNhh;><=|6#z;)o`J_9M3Y(cQ7)O>a^)GIauu%PJLyaWLYCx%x)DS<*qlU8OnhZ+#3{a~hq5Tpvx`&mJ z=xDxruti`iVTG8Tum}$6cVG}!ayb}+t+AyW#+I!Bcp;)3Rz#FNA5o1S_bf0v)9DtFTkNRxzkb=uS z6+#Xy?9&i}a!eFV76#5{r^lt0?9S9I6dt?8TrkiiZ0nvl+n)yplB@cOt9iLf+$b^} z(`8VmM1LzjG4KOTX>Fb$?(;!X@zXwY2Vr(*LxnU$g|rB4ubE`F)nj|SWUj5}<~P(0 zOh%2{tIGb6D9b1c0s-HCktV5Gs)1pt7EI&QQ*$j0zXZ38s357SmDi1}T>Zdm!KAd# zPDjwn@hQkse4$X3<5QI5GcQZ@^FeSHXES^r2+jrCd`f`3K?F>T zPE4;XAsc7g=8~Ss80o*=O5ZwToQS1=7@D}+LDRcHZUzAxce3ATyOH(N zT&gTe8HYxqt5S)hsl+j$|E!Ls8dN`fNpEejR5x)u|5#A@2_$rm?!Rkh(;B-NR z_#1y7)Q_A72Yui=$Vqh@Yy+w-2+&Uh;U=Pf4#9(;`V`P{O(=#A>22Q-i5fKqeo7BS z(`T+2z3uwZZ*cycnQ=B7%tocDR02&nL2=WwYR;J{rB`53!c_8DC4`%_5zWhJUPZGB z%rkhR4u-eFEw2JYOW5)c$Cmx^ZbjADidDcC%Zu-Gs1Qrb9?W+jb-41W!!!Ts%t7`u zLmD$X!UL&?>jMYqSQ;_Le?eed^ir(geP@v2ewGgOWXMqX)5R_=fY7RR6O zS~kk=7%yMZw{Gy=WO;pbbKAz)=Jqt{LvHT0B7MC*py zTK(oVz4M5E;;(pGzS5oo9Cx0B^RtThNMF1&as8;#eO+&Dhw+IE=Z&sJ*4=11qo3&3 z6W^vgkHNeX9p6txqp3va#Lw;N*5go%{B_L#M?V8$Crtz-l(7IpPN;ZNX&_1{B)_ z>PXcZ7ikvYTI2qOUZaQQ*QM>?Y=WB{67O3~Ces7X zY+@gAjA{M9n05bVo}aSKH#uY5rWp8{sw#r^hf@svOs#a8obi{Z82FhgD}lCSih-Z0 OY8Z@{{)#^2z5WZhp8+iZ literal 0 HcmV?d00001 diff --git a/api/models/__pycache__/prompt.cpython-312.pyc b/api/models/__pycache__/prompt.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc0694a6c4759dcb680352a7364323efb0475d24 GIT binary patch literal 7343 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 literal 0 HcmV?d00001 diff --git a/api/routers/__pycache__/tweet.cpython-312.pyc b/api/routers/__pycache__/tweet.cpython-312.pyc index f90c12e89beffda6613ac2e5bbb2f36164bffa57..43b84973edba7626481bd95b09a65b151b35363e 100644 GIT binary patch delta 55 zcmdmGaL9o7G%qg~0}y;_F37Oj$oorFP$01&Q!gd8AT=)~H7_|cwRp0C*g94(#)}M| Kn-7bXGXenlSrT#p delta 47 zcmX?Pu*-n=G%qg~0}#Zu=4WVa PromptService: + """获取提示词服务""" + return PromptService(config_manager) + +def get_prompt_builder( + config_manager: ConfigManager = Depends(get_config), + prompt_service: PromptService = Depends(get_prompt_service) +) -> PromptBuilderService: + """获取提示词构建服务""" + return PromptBuilderService(config_manager, prompt_service) + + +@router.get("/styles", response_model=StyleListResponse) +async def get_all_styles( + prompt_service: PromptService = Depends(get_prompt_service) +): + """获取所有内容风格""" + try: + styles_dict = prompt_service.get_all_styles() + # 将字典列表转换为StyleResponse对象列表 + styles = [StyleResponse(name=style["name"], description=style["description"]) for style in styles_dict] + return StyleListResponse(styles=styles) + except Exception as e: + logger.error(f"获取所有风格失败: {e}") + raise HTTPException(status_code=500, detail=f"获取风格列表失败: {str(e)}") + + +@router.get("/styles/{style_name}", response_model=StyleResponse) +async def get_style( + style_name: str, + prompt_service: PromptService = Depends(get_prompt_service) +): + """获取指定内容风格""" + try: + content = prompt_service.get_style_content(style_name) + return StyleResponse(name=style_name, description=content) + except Exception as e: + logger.error(f"获取风格 '{style_name}' 失败: {e}") + raise HTTPException(status_code=404, detail=f"未找到风格: {style_name}") + + +@router.post("/styles", response_model=StyleResponse) +async def create_or_update_style( + style: StyleRequest, + prompt_service: PromptService = Depends(get_prompt_service) +): + """创建或更新内容风格""" + try: + if not style.description: + # 如果没有提供描述,则获取现有的 + content = prompt_service.get_style_content(style.name) + return StyleResponse(name=style.name, description=content) + + success = prompt_service.save_style(style.name, style.description) + if not success: + raise HTTPException(status_code=500, detail=f"保存风格 '{style.name}' 失败") + + return StyleResponse(name=style.name, description=style.description) + except Exception as e: + logger.error(f"保存风格 '{style.name}' 失败: {e}") + raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}") + + +@router.get("/audiences", response_model=AudienceListResponse) +async def get_all_audiences( + prompt_service: PromptService = Depends(get_prompt_service) +): + """获取所有目标受众""" + try: + audiences_dict = prompt_service.get_all_audiences() + # 将字典列表转换为AudienceResponse对象列表 + audiences = [AudienceResponse(name=audience["name"], description=audience["description"]) for audience in audiences_dict] + return AudienceListResponse(audiences=audiences) + except Exception as e: + logger.error(f"获取所有受众失败: {e}") + raise HTTPException(status_code=500, detail=f"获取受众列表失败: {str(e)}") + + +@router.get("/audiences/{audience_name}", response_model=AudienceResponse) +async def get_audience( + audience_name: str, + prompt_service: PromptService = Depends(get_prompt_service) +): + """获取指定目标受众""" + try: + content = prompt_service.get_audience_content(audience_name) + return AudienceResponse(name=audience_name, description=content) + except Exception as e: + logger.error(f"获取受众 '{audience_name}' 失败: {e}") + raise HTTPException(status_code=404, detail=f"未找到受众: {audience_name}") + + +@router.post("/audiences", response_model=AudienceResponse) +async def create_or_update_audience( + audience: AudienceRequest, + prompt_service: PromptService = Depends(get_prompt_service) +): + """创建或更新目标受众""" + try: + if not audience.description: + # 如果没有提供描述,则获取现有的 + content = prompt_service.get_audience_content(audience.name) + return AudienceResponse(name=audience.name, description=content) + + success = prompt_service.save_audience(audience.name, audience.description) + if not success: + raise HTTPException(status_code=500, detail=f"保存受众 '{audience.name}' 失败") + + return AudienceResponse(name=audience.name, description=audience.description) + except Exception as e: + logger.error(f"保存受众 '{audience.name}' 失败: {e}") + raise HTTPException(status_code=500, detail=f"操作失败: {str(e)}") + + +@router.get("/scenic-spots", response_model=ScenicSpotListResponse) +async def get_all_scenic_spots( + prompt_service: PromptService = Depends(get_prompt_service) +): + """获取所有景区""" + try: + spots_dict = prompt_service.get_all_scenic_spots() + # 将字典列表转换为ScenicSpotResponse对象列表 + spots = [ScenicSpotResponse(name=spot["name"], description=spot["description"]) for spot in spots_dict] + return ScenicSpotListResponse(spots=spots) + except Exception as e: + logger.error(f"获取所有景区失败: {e}") + raise HTTPException(status_code=500, detail=f"获取景区列表失败: {str(e)}") + + +@router.get("/scenic-spots/{spot_name}", response_model=ScenicSpotResponse) +async def get_scenic_spot( + spot_name: str, + prompt_service: PromptService = Depends(get_prompt_service) +): + """获取指定景区信息""" + try: + content = prompt_service.get_scenic_spot_info(spot_name) + return ScenicSpotResponse(name=spot_name, description=content) + except Exception as e: + logger.error(f"获取景区 '{spot_name}' 失败: {e}") + raise HTTPException(status_code=404, detail=f"未找到景区: {spot_name}") + + +@router.post("/build-prompt", response_model=PromptBuilderResponse) +async def build_prompt( + request: PromptBuilderRequest, + prompt_builder: PromptBuilderService = Depends(get_prompt_builder) +): + """构建完整提示词""" + try: + # 根据请求中的step确定构建哪种类型的提示词 + step = request.step or "content" + + if step == "topic": + # 构建选题提示词 + # 从topic中提取必要的参数 + num_topics = request.topic.get("num_topics", 5) + month = request.topic.get("month", "7") + system_prompt, user_prompt = prompt_builder.build_topic_prompt(num_topics, month) + elif step == "judge": + # 构建审核提示词 + # 需要提供生成的内容 + content = request.topic.get("content", {}) + system_prompt, user_prompt = prompt_builder.build_judge_prompt(request.topic, content) + else: + # 默认构建内容生成提示词 + system_prompt, user_prompt = prompt_builder.build_content_prompt(request.topic, step) + + return PromptBuilderResponse( + system_prompt=system_prompt, + user_prompt=user_prompt + ) + except Exception as e: + logger.error(f"构建提示词失败: {e}") + raise HTTPException(status_code=500, detail=f"构建提示词失败: {str(e)}") \ No newline at end of file diff --git a/api/routers/tweet.py b/api/routers/tweet.py index 3e71c4b..2ef0b88 100644 --- a/api/routers/tweet.py +++ b/api/routers/tweet.py @@ -20,8 +20,8 @@ from api.models.tweet import ( PipelineRequest, PipelineResponse ) -# 从main.py中导入依赖 -from api.main import get_config, get_ai_agent, get_output_manager +# 从依赖注入模块导入依赖 +from api.dependencies import get_config, get_ai_agent, get_output_manager logger = logging.getLogger(__name__) diff --git a/api/services/__pycache__/poster.cpython-312.pyc b/api/services/__pycache__/poster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..135d452c5aeae3b33ef3253d697278490c5a5b73 GIT binary patch literal 5368 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 VZCG_o`*-ajrDDz31S;_Y{{-VxzZL)h literal 0 HcmV?d00001 diff --git a/api/services/__pycache__/prompt_service.cpython-312.pyc b/api/services/__pycache__/prompt_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf75200dfff59a755f3a167c7a2d4c98bee74f87 GIT binary patch literal 28523 zcmeHwe^eA#o^SP!>h5Y9XrO^^y1_<3XcZ76QBfeuPZiM!CXuKNQjIp@SE{=;kS60~ zk_?$45ht05`6bRI8yvHd?Bs1oOeRcB)a*O&oGM{n_g0*ajG_bdt5_YdwfGY4y)S|+6@hc_QZxn9M-fYwI??u z%LqAPm$fbv8yYyNAsnJ2?6D@V^}V+0^e2Uisj+lfUktdh?md(;sWE zp#p1dB(E%YG`o1kx{hvMv!%=B=u$#I{{-xpz(sSlG~Lna$50>vVo+P&kzkNnzXBF zm0jJUq}33sp*2#Nf-psErEnC4wX{wOM?*M@PN8F@nAm5?1|6-(;bZc$bRyJ|gzGW1sOV&fOAL!MLR?a}$!6xGh2E>P zGy5IQc6U6~FAcOX1FpP(nvZG5nJ3{10s-r&AOcY^}{ZtMf+2;q@=h(?@%9FMFO zrG*`|%q8AKgg6Hwmn7*STB)!s5IpIRk`XXl74CKjYOVfbvhR(_cYZb5|4LZnB>~B^ zj@j!hl0qnZ=(QDDuRZ(1l~3Qh^4{?)1J6w!9hPKy4db7VFu|EPsGisEwYwUH{7e*z zh!sWrv3zu+qr>59r1vzUSq?K;>9n`C;M`8si&pEc)%i?krz^jnY1(gZgPHEKceu(J zdy}h^$#3d%X}Crd0$L?bW1knv_3lEH@wz(d>U_AU_cee|Vhlhp{_C zP=rfLrDZ4m{$7~}o`gNn7rkUqv*5-=0K|&*AU{+3Xl9X;tjc2GCb#$Fh`_$ z)2(!&9><4APLs6iUG+)-3@Rjg6)x0Xe6+@hzQEa|5X_MPK0T0ZB1m zFXf?}vi?F3)oKn%h(56;xDDFgC>I)Ne6*&CzQDOPJ>VC89<3)zs!Qil(b~sy2!heU zxMoN)I;od*9-W8usMbPK|4%(?$^Wd9aM?up*2SVPaHe&1w16{6Vjuu&|J<9T)XI_O|z66BlhoW1f4RA zj5Z+ihEnvyGf}Z>Su3Gz4-Rj`$N1+c$)S^(@n0XJ@B&6Yjt*ZxUGv7g=k08FPztyIOKrosjVwdq;C84X~m|p7~@?%FfIua~|DYl)Gz* zEvK}oC>H{~;J20b^2)AG#^v~5I(U3X`5&?9S$&O!I(g;3PJk>_TW51q8}=&Q@ciYK z=#kB9x|*EM1Dy=b6TC)jFdyT>Pjd7IK#y`>-Mp`faoWKHZClm8XN66}Q>ccq&6ZbgDYcN2qdU1ljbZZt?zA4okG=*+%>$zLA(*H8L|q{+S!D=#Z@ zqnkM(BX(gR`&;i{{ow5)tIfoqY0azbPXI=9@~XDZy$FJ-4>U0y(2Km<{|e)kon7`0 zUWtZ1yg|w(|<7i{a&rohRFyT+RD<)n2C4-R0!volahD z@7V8PAh!bEp1i^VU8H@esm*P#V3wL zJ(C1Zk2&l$9RdzOz?lp$6?j)Gmr>TsrqscGuch^E^rdDG=X0s6`)V&Gql}K z>?hhUrL7sLx@bu|R(-U3=;2}exMjsa1)ps3Em?MI?TNJ~OE_cBpn53z;^L*J7N1yr zD(^(zX!2P0_~MO&v6swgLyKN5o|Y?8S6*CRIMO-3ymBZOphm$1GS-+goN&RIdueF_ zYseg`yI`qxs@rP=juVKw4qmPd^v!?YEx-y@6L0>tF{eGGRkz39tm>^G=RxKj_B#%L^ zKB-Es_|u|wDDZjVDsa!_MODSh&#W%fK;WF5tkS8^sj)vct}25(XO*M)g)-#NP$2&z zs@kYHm%pGoL2*8T1V68Jy1Lu!!m~-m*j;X>BLwfG#{|MzH`3q%33#7qg+Tbh3{bPA z%!LUtW<-zOu!EMpE8)Oig-0<9UX-=S#J9AJ0I4~?A$paNF5$;Obt0~U{PKCyl}xG& z-WkHt8Y}t&XTYnn(D$TANt5UcuMW^N?jH#)26DBc=S0{M2`l!hJSxDUb$~(DP?ij1 z%o-St2xt*drHIcp9!+a%Sn&`PubWvi&oyagT$84C!OZe#O02ON@JSufzgRdShv;h=U!E!Z>_*glhAe`CmoB+)xRNq)c`(>0~DwiWdPS@^pZiPNZ%}?1VPX$ z4;eaN0m-R*$)`v_mX8R&0G10YebKz7YdvbX_9$FO&~*mYvG?Dhj&<|Yv2fn&h?%XI z`p3h|w#Bun*C~)<~4<`@3GWp!Ilka_W?RPIu4Sgc1D>@Ma$?wd`SgncyoxV9tokA=9~D%#8%^zb zGfnytYpomILr5Hd2weHY=_{v?2|YZc8U}q7e~TWyS5KXWA@B>}j6!+VTk;|+ZosvJ zuU`G^7gNK>ul(Vcll>pT$Hvt2e*`o=qtIJQYi^0vjh?0vs}&pSP63Ij*MC1tG2bRa zGQjr9k>@Ud`q`D2hp(L;y!zf-lLtR>qnjR@gW~XT+Y*@-5XHy%)lPmy3?EHy#tG0_ zZGXbv>~`6CO^Y2+cV`FS@8-5n_+VKM*_aCCR3fJeIjCNq#N)=32pT*2m_}4`i@U9@ z5k)ZQ>yM{E7ezO^>`%ZKjKkU3;%I|n+d2=R&mbote_n~cW0?Cu)~3byo2f_nlrT#= z7^Dm20Y^9iFEjAAf{!d=9t$)cn*8K$;-Lq<>SdK29!Qi>M`?C z^N4cLJZ>r+Pyv)iFxzL?;!9jGu=ysbh>Hj0oMJt;;OK&J(_%qFcUbpwj8AVkqCKpA zS@+Kgri+G@BbyFyVi#77wT|s%t=lFHbv|?YP%dX)#u}DQlY}8tzq_)^lmDS2E< zo;PI$m$Jf}QpBYc^= zaONcw=G=>xg~v7>-NY{5%0BogyLBgcj2BdKX82n0H>ZxR-_6!FvW?9w-R`9s zj%L_qC!6D%u(*90i(jq%?s_aV=4J{(#U6@zK87{jH?nbLEn8a8CU2k6JmiZ_{qANd zVMqn&48w)Kd7!J=1=Z){&MVl|8n1ry1^wpl0Zv2e_fU*69VFqhQ2*B)u3#IiNMis1Styfpf>55S^sFGVZeht&@ z#r4ruF~sL-SsT)cGs<<*RmJiz;^Ywc!k|X3Wqo{gzWi(+iaJ|>+;cHhRgvOcvazaA zajuX={$i?nDS2LHuFfXUr>j8bd^U+wAH8B90*7@HKempNWY(7XIR&|k;*#VW!0_DSmMT9&Dln=`R#fL$8zd$gQXC4(> zFdLSiS?W%dB>?5a1jL4hLisQuXd42W*oH2kb^z{EeIH(V`Sj$^5Bae>17s2cgJR|< z$cJ=#%pT;-3ei=x9qjHjLnA3Wau6(MT9C6BIs1^~Kn_yHg#d9Uig*k;2!}Im$iX07 zClrcdzLV)d@tw%&0w*HuHXu|jP72Ys(X((S@(z~)jSt~$xl8a}?@L?=O5z`YzT3wd zS?l%*!$XnLw|RlroX?r_1%0#Eyp}Vs?W_6Pg7L9{zB7k4T=KHpMc=#Gt&MEXPbMr) zkNw=qwgHAIgc~vjhmN^Z1}MD{aWveYHmgK_==63d1Em8Zer{9uzOnAyueZQ0M}9GI{?5kTLz5cIuyq?BF%Fq{0ygl2~?Hsk*}^vy6Z-+0Q|)#<`I?8rE` z2Q=9Cbl1>RaNN^f3;iIvk#yG~8rTSWXVB?}>H524!Zf7wYZhnKzxIC=u2~E#J8NF8 z(JX_7;5V1smcD2FMoD?>Sw26AnHHC8^B6dxV)K~%;5S&pd>0WCCfZklm<(R z;G^3J?h?CI$Q&T!2`g8=2B2y=AaOk@Z_-0h~HAWQ)TZ z$sdAhI`_1KK|dm@!Ky?`P$hOlMUfGVP(V0>aZ->$(isXWLyTR75o^RR2E-nSgk8dA z#-S|dn_xQwlYFq^aMPVA4r_dgX#+KP3|-cbtsYy#T5Bgj^Eg9ssH4_^d3avvVjZgH zl9%2VblH5~asE*@XUBx4VJ2|74r)UiXv;(;-LetWNEDmAc0yC^i#2?AGuyAB+Z52y zRg5)_)v>7?y!wrtek0b+*EtZ`d&>jpy6 zpm8f-hBR)Ku~_3)k%lyG5ctA?-DOmTS@FecV?~nU%On!~U#3x&I`YeGbETR*Df_w;As@BZ7@pWVY1^CFi! z!xhJ1VoI1nc(+~t_?^j@es=q~BKR&ZvI@EH7+M5ZdWX=$U%ww$_(StSi{Sd^h8Dq+ z;;ViRXb~*#hZgRDAyr776IuipAl4TOErKPW3J(n7562a1e}~NrEPe*nLA;5hQ4mG?z?L6?DAod1(E8wnVOwNGVX=BG z%Q(w2uVocyS>?5qaF&w3&0kxvxul6Zz|J%Yjg!~j-YsS2_Qn+mCx)EhxC=*>xc|J0``1Ga8=UT>U)&Axw+OJ(h4VSk6fz_dUpuoGv6JxSj#TFyMv1qUmmt-KXrw%w*Uu4i%*Sh zKwD>?gH`~aA5m8VigK(Y$J%nN9w)zG zL~w~E?nH~hS{hMW;01w|=UBb&ME8~S5gNi=38^9yb0tKe0KF~>!GRA@17TX+7d@?t zhi^Yqs=#>mn(pIF_jyevoT;R5<3Fb_xuH;4WBxIH$)EvITe^SUJyibAhT|JX3>PvB zzfllzi9;#V3L-jnngF>QS|T-9P(|0td<(LV^&agVs19d@T)It0h%sw;#R&Cb%=isjY_Zvev*lB9$BeaU=*vFHN~%idUN2)Yon8=KAP%#<;v+dz;pG(Q%7?2*cOw!zNL-3d@Wy6v zu^Hajf(x+)7vn9!MjDUb%<48@G#CdB;_bPHGp!k22Y2VVp|p=eNye9I`zW6!%WEm% zECmym`}(?BO~a^d%)u3H@veS^Tm8uR>IR5nka{j;38qB7_hIwJUILa|MyB8YW>!>! zW@*?Vj_BRd@5Ood??S!L45BheKp?czCE*T_L^j4)B8VdGQA$L72{Qz`CKBns6%9$) z;V0rBhC2=s@rg+E`Y=od7%%(~$AD=EG?X5m8br|F1?d?x(`k*^|3GYyNL}yM(ttvM zq#p0lwq{2t0hY6i%O`rZK#sS~l<{bT`c1t>8+khMbw;po?c zFi-zW7{AHt5NeYUAE35S;{xKPh8?q0)B7)l8yCXL4mFKLDmw-f(_?2NruYA!d1?qH z)<>#AKbvX91AP%*x-HKA3|ziegb#iN{qqN>UVmOde*zg;5|BI*;hG>k*>~jX@nHaf zQ>PAH9{v5~vp>J`!O&#?vy%fS5qTFja0udIO9j>o#F3SLBS?Va-Q={QY)j{6hP$nB z9TBV`_)>&+`7H_1B#F%(W(Wxq76D)_hL&!0-vfG_I^HKSZWuIYuhHP#Qsm|3osbKR z2*gNX8jypTA?PL!?p1#mi7O!ml69C#!%wMhYHJH)Yw(ITdq)_n8gDrfu7C)@PzThH zUQ@7N1uT_RHFd#)O4<)Zc@^$uXFxN7UuZy{c4B17w4=)i2s zuQ$x;kWzP|*Bb{8fXx^p-q5Flg?b`eS{Ho>Mh|wuBNO@^J|IcwAI%?6Su&tTH0p4Y zbc#fhUjM~?V+YP4?ZnnP)>=Pd*zPkg9H>PsY4dbU901-qS<b&|)4O{onh4P1nSB|V0 zDH|>xExfSg0d{BebyAU?a+6S)%->y)14{GsLPDR^7yaGM{M)vTWZniF`8I7M7zro# zMf-v94K(iu*2h<*5uayeRRX4sS|3xXlYeQFLjV~-G~r!rP$D$Zq+;J=4bpnvfpzQ-|{_#R3TyTyOLhf>6? z5dJHplwdNfge4?TM3f>>DenrUto5T5k$;FSCbI*TP}1w1P(^4QSSF6ZG@cWu_%B0T zp(fxIw2BMilq1hxd;Q?l`%hz#!XR2Ca}+tRBFBxKi0CA=%vorpuc7RFkQ1PhB8p8o zFxijd5I%~iA49-|c>_6bf+HdlO6<`fP{Ir$DWqOu4j|{Y0SR8cg#d|EXe1q2h`_H9 zESV5tNn&yz^}}Jwt_l6_9}||$$-($>V2N$Go--HtVaZ6$n0!2=455;AuQ3O9Ubqch zvN#Br*bpu;u^QulBrq{T{eEP^{1&E9WcXlIB30TPN{;jY#g!Zn|2RqxFfNgP;}U!x z^^zXgD+^ghfhTXjP#HM6Qxf)yQ9q&5B`1Y@?t-dX9w6$QIedIh9MQl&(T{eaaVS;2V) zKjd39Gd8F{r9GScJ*n}4@+GJbWe|qm6YG%?{ZERj5x;4R#HOuKC5b9%K15P8z%E`h z7?@PA>M2$K|A4*6;2dJW3NAaVcfPV?JQ`4+#o`(yg&Hx|{tNR%5sXBbCn0}ikP#|vGel%{-Y&@KMb*p-G7z-Dt5rmNK_W`#3~bSl#4x}> z104o1A}c$DB`Qdrfyud;>gkL`^TY~~AeoO5!Z3rW0V_V@68gv>gb{mZijVCR`iJfY zVO-Y`8H;bI6_zE}Rp1-U1SLp%*3d)8br);4uv_cd?R&Vb&EqvRySMd1P3w?rcr%w> zG`eE6jLm*vtnfm1HQVKajfnQk%H-Dx;&HS|5vfQ`f-A~cd>xewroY4`f@;MOGarwX zx(n5cG4+`4sLq#}`%dlg+WC|xi$stT1ys{%3!zW!)BW2w1<>B_Zfb5@g_8Cot56Jm zI?&Mh)lD;4LJB=`e2esi$V zMg{Fa;Tzj9KAOb<^uw?bG7d`SNT|bqh;K|xOpy$eVk+PT*!44H?ZVE~|i;A{m@$73_zq3-lc9*ok)@ zdPCToRpXLOP#z81hx1+iN&g&!y;EW5(bfpK$rFTEXxIxGlsWLoM29SR&rk`U&2p8Q zLY`nZ5}MjCV^-Z=36Ts~L3^7@?L6s6y>JzCk7^vkK*2lGcB{hFI0)qg%7}!~)DcjQ z24OSUV<{epKw&5?bcfIW52PJCgVEciHK*mZ)4c3}fF02T11~@dZ&=V`B(ZJt z$%(D2oJE_}gxlQQ+1~DOg=@7ieX9xtF!ciyLm05V!%jx+j{z-&Da8H*3{bO}A|%#~ z9E?V>Qp>+X5vGFeKuM1wrx6_3E2-1n;oqc)!efPqCd_Y;z(#N)u8Y<>VcTxOyw+n3 zC65GBt|5T=zrg{3X_QQAY0(w4uOoUbbpV;);*y2khhPzrX`o7^{F)XVi#-}UZnO=j z!!DKs~i>TuM{(KGFHXs;beV$(`SU860mrD8%~37oQNZ9HYVY_ts4ds7Rz)B>2@)F%wA{?$`M_1>)2 zT-IuDRwdmU);D2ldY`o;vXL9;XuYNJ7U+mQ{SR;Py!!GB`f|K>sX}(^uSw7pHV(G! zjQ4NbX_A&D1#R1TF#&X2uwkd^)(tdiUMy3=Y(0~-&QP_6_+oKpopH71R9two+1gvon1>Qq}Kux?& z9C;4~qPqqi^j)LCxea@>p+LU@Yvxv!At+Fg`cYBf8oy#L6a|JF{$|_lMu39r{ZKG0 z&m3S-G;0O$U}Z2K3@a!nSo}6Xq0mY5fx_9OMA%i!AW+Es9vog(E^ID?792>7D$!C2 z_>Vb-MAMKXA;9qRXG4H<$casv3js1CsMau}=4|kfc^?TQ@W=ceaxnA@SUcx|e#{?G z9;9uA{ZDa++y?TC#`sPg!SJo!LlT$3hqCiu?*{)pct`ke=WO`zCqEMY0~ASn)0T5- z%e`q2aA^;W#c^q60Le1OY(z?)Uc!T;FGAOyIC(=YMr3pjm&SHFtWulgimbjRtm z34Qf{00LzG9;Q^SO@>YWwq*2sJ?P80v60s{HnziJa$ur2^18;x$K6eB{uFg%Bi-5D z*vM$noJ3|h{L=x#jD8#ipFqw}k@GBa4j~6o!v&i@(ho97nZq1M&fCZt28UPffnQEw z{u>H^49*Dx6U+tfJK89kK@O_VX@Qyg4DpXj%d|3zOdaaILBQ#ol;xy#dY^1HnKoQD zO~7fS>;^hb?}xa|kqi`qat$A2|`jWvoOgCZWb58rG#KM(Rd1CrY?QMPgeXkddZAr~{ox5`^=p;RX&*m&;M~ z5L^>ZF7qds-^5{|<4||mEX*I_#H-QIWx&s5F!w`l~4{e-_mqx%xEs79+2qUVwld+>T_=PlKa)26;;G^9x zN1HQG_`-KGh;70k-KCg-mpeE*8KjruDPVlo;9vG&P9wqP$iag@hI}c=L8?h!vsRd% zCCooS5at8ntOW-a;mBmyH42#$)(**Hzb2x-CMfv-PsH*+6U+ZX7{4Zx{z7D2AqxMA mSo0Sm=Mt6Dm+YldI4Wgu)i{-TQ1w^UHcAFRdVpM9-~R Tuple[str, str]: + """ + 构建内容生成提示词 + + Args: + topic: 选题信息 + step: 当前步骤,用于过滤参考内容 + + Returns: + 系统提示词和用户提示词的元组 + """ + # 加载系统提示词和用户提示词模板 + system_prompt_path = self.content_config.content_system_prompt + user_prompt_path = self.content_config.content_user_prompt + + # 创建提示词模板 + template = PromptTemplate(system_prompt_path, user_prompt_path) + + # 获取风格内容 + style_filename = topic.get("style", "") + style_content = self.prompt_service.get_style_content(style_filename) + + # 获取目标受众内容 + demand_filename = topic.get("target_audience", "") + demand_content = self.prompt_service.get_audience_content(demand_filename) + + # 获取景区信息 + object_name = topic.get("object", "") + object_content = self.prompt_service.get_scenic_spot_info(object_name) + + # 获取产品信息 + product_name = topic.get("product", "") + product_content = self.prompt_service.get_product_info(product_name) + + # 获取参考内容 + refer_content = self.prompt_service.get_refer_content(step) + + # 构建系统提示词 + system_prompt = template.get_system_prompt() + + # 构建用户提示词 + user_prompt = template.build_user_prompt( + style_content=f"{style_filename}\n{style_content}", + demand_content=f"{demand_filename}\n{demand_content}", + object_content=f"{object_name}\n{object_content}", + product_content=f"{product_name}\n{product_content}", + refer_content=refer_content + ) + + return system_prompt, user_prompt + + def build_topic_prompt(self, num_topics: int, month: str) -> Tuple[str, str]: + """ + 构建选题生成提示词 + + Args: + num_topics: 要生成的选题数量 + month: 月份 + + Returns: + 系统提示词和用户提示词的元组 + """ + # 从配置中获取选题提示词模板路径 + topic_config = self.config_manager.get_config('topic_gen', dict) + if not topic_config: + raise ValueError("未找到选题生成配置") + + system_prompt_path = topic_config.get("topic_system_prompt", "") + user_prompt_path = topic_config.get("topic_user_prompt", "") + + if not system_prompt_path or not user_prompt_path: + raise ValueError("选题提示词模板路径不完整") + + # 创建提示词模板 + template = PromptTemplate(system_prompt_path, user_prompt_path) + + # 获取风格列表 + styles = self.prompt_service.get_all_styles() + style_content = "Style文件列表:\n" + "\n".join([f"- {style['name']}" for style in styles]) + + # 获取目标受众列表 + audiences = self.prompt_service.get_all_audiences() + demand_content = "Demand文件列表:\n" + "\n".join([f"- {audience['name']}" for audience in audiences]) + + # 获取参考内容 + refer_content = self.prompt_service.get_refer_content("topic") + + # 获取景区信息列表 + spots = self.prompt_service.get_all_scenic_spots() + object_content = "Object信息:\n" + "\n".join([f"- {spot['name']}" for spot in spots]) + + # 构建系统提示词 + system_prompt = template.get_system_prompt() + + # 构建创作资料 + creative_materials = ( + f"你拥有的创作资料如下:\n" + f"{style_content}\n\n" + f"{demand_content}\n\n" + f"{refer_content}\n\n" + f"{object_content}" + ) + + # 构建用户提示词 + user_prompt = template.build_user_prompt( + creative_materials=creative_materials, + num_topics=num_topics, + month=month + ) + + return system_prompt, user_prompt + + def build_judge_prompt(self, topic: Dict[str, Any], content: Dict[str, Any]) -> Tuple[str, str]: + """ + 构建内容审核提示词 + + Args: + topic: 选题信息 + content: 生成的内容 + + Returns: + 系统提示词和用户提示词的元组 + """ + # 从配置中获取审核提示词模板路径 + system_prompt_path = self.content_config.judger_system_prompt + user_prompt_path = self.content_config.judger_user_prompt + + # 创建提示词模板 + template = PromptTemplate(system_prompt_path, user_prompt_path) + + # 获取景区信息 + object_name = topic.get("object", "") + object_content = self.prompt_service.get_scenic_spot_info(object_name) + + # 获取产品信息 + product_name = topic.get("product", "") + product_content = self.prompt_service.get_product_info(product_name) + + # 获取参考内容 + refer_content = self.prompt_service.get_refer_content("judge") + + # 构建系统提示词 + system_prompt = template.get_system_prompt() + + # 格式化内容 + import json + tweet_content = json.dumps(content, ensure_ascii=False, indent=4) + + # 构建用户提示词 + user_prompt = template.build_user_prompt( + tweet_content=tweet_content, + object_content=object_content, + product_content=product_content, + refer_content=refer_content + ) + + return system_prompt, user_prompt \ No newline at end of file diff --git a/api/services/prompt_service.py b/api/services/prompt_service.py new file mode 100644 index 0000000..076fd57 --- /dev/null +++ b/api/services/prompt_service.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +提示词服务层 +负责提示词的存储、检索和构建 +""" + +import logging +import json +import os +import re +from typing import Dict, Any, Optional, List, cast +from pathlib import Path +import mysql.connector +from mysql.connector import pooling + +from core.config import ConfigManager, ResourceConfig +from utils.prompts import BasePromptBuilder, PromptTemplate +from utils.file_io import ResourceLoader + +logger = logging.getLogger(__name__) + + +class PromptService: + """提示词服务类""" + + def __init__(self, config_manager: ConfigManager): + """ + 初始化提示词服务 + + Args: + config_manager: 配置管理器 + """ + self.config_manager = config_manager + self.resource_config: ResourceConfig = config_manager.get_config('resource', ResourceConfig) + + # 初始化数据库连接池 + self._init_db_pool() + + def _init_db_pool(self): + """初始化数据库连接池""" + try: + # 尝试直接从配置文件加载数据库配置 + config_dir = Path("config") + db_config_path = config_dir / "database.json" + + if not db_config_path.exists(): + logger.warning(f"数据库配置文件不存在: {db_config_path}") + self.db_pool = None + return + + # 加载配置文件 + with open(db_config_path, 'r', encoding='utf-8') as f: + db_config = json.load(f) + + # 处理环境变量 + processed_config = {} + for key, value in db_config.items(): + if isinstance(value, str) and "${" in value: + # 匹配 ${ENV_VAR:-default} 格式 + pattern = r'\${([^:-]+)(?::-([^}]+))?}' + match = re.match(pattern, value) + if match: + env_var, default = match.groups() + processed_value = os.environ.get(env_var, default) + # 尝试转换为数字 + if key == "port": + try: + processed_value = int(processed_value) + except (ValueError, TypeError): + processed_value = 3306 + processed_config[key] = processed_value + else: + processed_config[key] = value + + # 创建连接池 + self.db_pool = pooling.MySQLConnectionPool( + pool_name="prompt_pool", + pool_size=5, + host=processed_config.get("host", "localhost"), + user=processed_config.get("user", "root"), + password=processed_config.get("password", ""), + database=processed_config.get("database", "travel_content"), + port=processed_config.get("port", 3306), + charset=processed_config.get("charset", "utf8mb4") + ) + logger.info(f"数据库连接池初始化成功,连接到 {processed_config.get('host')}:{processed_config.get('port')}") + except Exception as e: + logger.error(f"初始化数据库连接池失败: {e}") + self.db_pool = None + + def get_style_content(self, style_name: str) -> str: + """ + 获取内容风格提示词 + + Args: + style_name: 风格名称 + + Returns: + 风格提示词内容 + """ + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT description FROM contentStyle WHERE styleName = %s", + (style_name,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"从数据库获取风格提示词: {style_name}") + return result["description"] + except Exception as e: + logger.error(f"从数据库获取风格提示词失败: {e}") + + # 回退到文件系统 + try: + style_paths = self.resource_config.style.paths + for path_str in style_paths: + try: + if style_name in path_str: + full_path = self._get_full_path(path_str) + if full_path.exists(): + logger.info(f"从文件系统获取风格提示词: {style_name}") + return full_path.read_text('utf-8') + except Exception as e: + logger.error(f"读取风格文件失败 {path_str}: {e}") + + # 如果没有精确匹配,尝试模糊匹配 + for path_str in style_paths: + try: + full_path = self._get_full_path(path_str) + if full_path.exists() and full_path.is_file(): + content = full_path.read_text('utf-8') + if style_name.lower() in full_path.stem.lower(): + logger.info(f"通过模糊匹配找到风格提示词: {style_name} -> {full_path.name}") + return content + except Exception as e: + logger.error(f"读取风格文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取风格提示词失败: {e}") + + logger.warning(f"未找到风格提示词: {style_name},将使用默认值") + return "通用风格" + + def get_audience_content(self, audience_name: str) -> str: + """ + 获取目标受众提示词 + + Args: + audience_name: 受众名称 + + Returns: + 受众提示词内容 + """ + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT description FROM targetAudience WHERE audienceName = %s", + (audience_name,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"从数据库获取受众提示词: {audience_name}") + return result["description"] + except Exception as e: + logger.error(f"从数据库获取受众提示词失败: {e}") + + # 回退到文件系统 + try: + demand_paths = self.resource_config.demand.paths + for path_str in demand_paths: + try: + if audience_name in path_str: + full_path = self._get_full_path(path_str) + if full_path.exists(): + logger.info(f"从文件系统获取受众提示词: {audience_name}") + return full_path.read_text('utf-8') + except Exception as e: + logger.error(f"读取受众文件失败 {path_str}: {e}") + + # 如果没有精确匹配,尝试模糊匹配 + for path_str in demand_paths: + try: + full_path = self._get_full_path(path_str) + if full_path.exists() and full_path.is_file(): + content = full_path.read_text('utf-8') + if audience_name.lower() in full_path.stem.lower(): + logger.info(f"通过模糊匹配找到受众提示词: {audience_name} -> {full_path.name}") + return content + except Exception as e: + logger.error(f"读取受众文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取受众提示词失败: {e}") + + logger.warning(f"未找到受众提示词: {audience_name},将使用默认值") + return "通用用户画像" + + def get_scenic_spot_info(self, spot_name: str) -> str: + """ + 获取景区信息 + + Args: + spot_name: 景区名称 + + Returns: + 景区信息内容 + """ + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT description FROM scenicSpot WHERE spotName = %s", + (spot_name,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"从数据库获取景区信息: {spot_name}") + return result["description"] + except Exception as e: + logger.error(f"从数据库获取景区信息失败: {e}") + + # 回退到文件系统 + try: + object_paths = self.resource_config.object.paths + for path_str in object_paths: + try: + if spot_name in path_str: + full_path = self._get_full_path(path_str) + if full_path.exists(): + logger.info(f"从文件系统获取景区信息: {spot_name}") + return full_path.read_text('utf-8') + except Exception as e: + logger.error(f"读取景区文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取景区信息失败: {e}") + + logger.warning(f"未找到景区信息: {spot_name}") + return "无" + + def get_product_info(self, product_name: str) -> str: + """ + 获取产品信息 + + Args: + product_name: 产品名称 + + Returns: + 产品信息内容 + """ + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT description FROM product WHERE productName = %s", + (product_name,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"从数据库获取产品信息: {product_name}") + return result["description"] + except Exception as e: + logger.error(f"从数据库获取产品信息失败: {e}") + + # 回退到文件系统 + try: + product_paths = self.resource_config.product.paths + for path_str in product_paths: + try: + if product_name in path_str: + full_path = self._get_full_path(path_str) + if full_path.exists(): + logger.info(f"从文件系统获取产品信息: {product_name}") + return full_path.read_text('utf-8') + except Exception as e: + logger.error(f"读取产品文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取产品信息失败: {e}") + + logger.warning(f"未找到产品信息: {product_name}") + return "无" + + def get_refer_content(self, step: str = "") -> str: + """ + 获取参考内容 + + Args: + step: 当前步骤,用于过滤参考内容 + + Returns: + 参考内容 + """ + refer_content = "参考内容:\n" + + # 从文件系统获取参考内容 + try: + refer_list = self.resource_config.refer.refer_list + filtered_configs = [ + item for item in refer_list + if not item.step or item.step == step + ] + + for ref_item in filtered_configs: + try: + path_str = ref_item.path + full_path = self._get_full_path(path_str) + + if full_path.exists() and full_path.is_file(): + content = full_path.read_text('utf-8') + refer_content += f"--- {full_path.name} ---\n{content}\n\n" + except Exception as e: + logger.error(f"读取参考文件失败 {ref_item.path}: {e}") + except Exception as e: + logger.error(f"获取参考内容失败: {e}") + + return refer_content + + def _get_full_path(self, path_str: str) -> Path: + """根据基准目录解析相对或绝对路径""" + if not self.resource_config.resource_dirs: + raise ValueError("Resource directories list is empty in config.") + base_path = Path(self.resource_config.resource_dirs[0]) + file_path = Path(path_str) + return file_path if file_path.is_absolute() else (base_path / file_path).resolve() + + def get_all_styles(self) -> List[Dict[str, str]]: + """ + 获取所有内容风格 + + Returns: + 风格列表,每个元素包含name和description + """ + styles = [] + + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT styleName as name, description FROM contentStyle") + results = cursor.fetchall() + cursor.close() + conn.close() + + if results: + logger.info(f"从数据库获取所有风格: {len(results)}个") + return results + except Exception as e: + logger.error(f"从数据库获取所有风格失败: {e}") + + # 回退到文件系统 + try: + style_paths = self.resource_config.style.paths + for path_str in style_paths: + try: + full_path = self._get_full_path(path_str) + if full_path.exists() and full_path.is_file(): + content = full_path.read_text('utf-8') + name = full_path.stem + if "文案提示词" in name: + name = name.replace("文案提示词", "") + styles.append({ + "name": name, + "description": content[:100] + "..." if len(content) > 100 else content + }) + except Exception as e: + logger.error(f"读取风格文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取所有风格失败: {e}") + + return styles + + def get_all_audiences(self) -> List[Dict[str, str]]: + """ + 获取所有目标受众 + + Returns: + 受众列表,每个元素包含name和description + """ + audiences = [] + + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT audienceName as name, description FROM targetAudience") + results = cursor.fetchall() + cursor.close() + conn.close() + + if results: + logger.info(f"从数据库获取所有受众: {len(results)}个") + return results + except Exception as e: + logger.error(f"从数据库获取所有受众失败: {e}") + + # 回退到文件系统 + try: + demand_paths = self.resource_config.demand.paths + for path_str in demand_paths: + try: + full_path = self._get_full_path(path_str) + if full_path.exists() and full_path.is_file(): + content = full_path.read_text('utf-8') + name = full_path.stem + if "文旅需求" in name: + name = name.replace("文旅需求", "") + audiences.append({ + "name": name, + "description": content[:100] + "..." if len(content) > 100 else content + }) + except Exception as e: + logger.error(f"读取受众文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取所有受众失败: {e}") + + return audiences + + def get_all_scenic_spots(self) -> List[Dict[str, str]]: + """ + 获取所有景区 + + Returns: + 景区列表,每个元素包含name和description + """ + spots = [] + + # 优先从数据库获取 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT spotName as name, description FROM scenicSpot") + results = cursor.fetchall() + cursor.close() + conn.close() + + if results: + logger.info(f"从数据库获取所有景区: {len(results)}个") + return [{ + "name": item["name"], + "description": item["description"][:100] + "..." if len(item["description"]) > 100 else item["description"] + } for item in results] + except Exception as e: + logger.error(f"从数据库获取所有景区失败: {e}") + + # 回退到文件系统 + try: + object_paths = self.resource_config.object.paths + for path_str in object_paths: + try: + full_path = self._get_full_path(path_str) + if full_path.exists() and full_path.is_file(): + content = full_path.read_text('utf-8') + spots.append({ + "name": full_path.stem, + "description": content[:100] + "..." if len(content) > 100 else content + }) + except Exception as e: + logger.error(f"读取景区文件失败 {path_str}: {e}") + except Exception as e: + logger.error(f"获取所有景区失败: {e}") + + return spots + + def save_style(self, name: str, description: str) -> bool: + """ + 保存内容风格 + + Args: + name: 风格名称 + description: 风格描述 + + Returns: + 是否保存成功 + """ + # 优先保存到数据库 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor() + + # 检查是否存在 + cursor.execute( + "SELECT COUNT(*) FROM contentStyle WHERE styleName = %s", + (name,) + ) + count = cursor.fetchone()[0] + + if count > 0: + # 更新 + cursor.execute( + "UPDATE contentStyle SET description = %s WHERE styleName = %s", + (description, name) + ) + else: + # 插入 + cursor.execute( + "INSERT INTO contentStyle (styleName, description) VALUES (%s, %s)", + (name, description) + ) + + conn.commit() + cursor.close() + conn.close() + logger.info(f"风格保存到数据库成功: {name}") + return True + except Exception as e: + logger.error(f"风格保存到数据库失败: {e}") + + # 回退到文件系统 + try: + # 确保风格目录存在 + style_dir = Path(self.resource_config.resource_dirs[0]) / "resource" / "prompt" / "Style" + style_dir.mkdir(parents=True, exist_ok=True) + + # 保存文件 + file_path = style_dir / f"{name}文案提示词.md" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(description) + + # 更新配置 + if str(file_path) not in self.resource_config.style.paths: + self.resource_config.style.paths.append(str(file_path)) + + logger.info(f"风格保存到文件系统成功: {file_path}") + return True + except Exception as e: + logger.error(f"风格保存到文件系统失败: {e}") + return False + + def save_audience(self, name: str, description: str) -> bool: + """ + 保存目标受众 + + Args: + name: 受众名称 + description: 受众描述 + + Returns: + 是否保存成功 + """ + # 优先保存到数据库 + if self.db_pool: + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor() + + # 检查是否存在 + cursor.execute( + "SELECT COUNT(*) FROM targetAudience WHERE audienceName = %s", + (name,) + ) + count = cursor.fetchone()[0] + + if count > 0: + # 更新 + cursor.execute( + "UPDATE targetAudience SET description = %s WHERE audienceName = %s", + (description, name) + ) + else: + # 插入 + cursor.execute( + "INSERT INTO targetAudience (audienceName, description) VALUES (%s, %s)", + (name, description) + ) + + conn.commit() + cursor.close() + conn.close() + logger.info(f"受众保存到数据库成功: {name}") + return True + except Exception as e: + logger.error(f"受众保存到数据库失败: {e}") + + # 回退到文件系统 + try: + # 确保受众目录存在 + demand_dir = Path(self.resource_config.resource_dirs[0]) / "resource" / "prompt" / "Demand" + demand_dir.mkdir(parents=True, exist_ok=True) + + # 保存文件 + file_path = demand_dir / f"{name}文旅需求.md" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(description) + + # 更新配置 + if str(file_path) not in self.resource_config.demand.paths: + self.resource_config.demand.paths.append(str(file_path)) + + logger.info(f"受众保存到文件系统成功: {file_path}") + return True + except Exception as e: + logger.error(f"受众保存到文件系统失败: {e}") + return False \ No newline at end of file