From 495b7525aeb5695ec99ee221d7a2d772ef68299c Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Fri, 11 Jul 2025 17:55:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E5=92=8C=E5=AE=A1=E6=A0=B8=E7=9A=84=E4=BC=A0?= =?UTF-8?q?=E5=8F=82=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=8F=AF=E9=80=89?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/models/__pycache__/tweet.cpython-312.pyc | Bin 8128 -> 9356 bytes api/models/tweet.py | 57 ++-- api/routers/__init__.py | 2 +- .../__pycache__/__init__.cpython-312.pyc | Bin 184 -> 184 bytes api/routers/__pycache__/tweet.cpython-312.pyc | Bin 8370 -> 9109 bytes api/routers/tweet.py | 32 +- .../prompt_builder.cpython-312.pyc | Bin 11923 -> 18612 bytes .../__pycache__/tweet.cpython-312.pyc | Bin 12402 -> 15616 bytes api/services/prompt_builder.py | 190 ++++++++++++ api/services/tweet.py | 107 +++++-- .../__pycache__/manager.cpython-312.pyc | Bin 12412 -> 12424 bytes core/config/manager.py | 2 +- core/config/manager.py.backup | 281 ++++++++++++++++++ 13 files changed, 627 insertions(+), 44 deletions(-) create mode 100644 core/config/manager.py.backup diff --git a/api/models/__pycache__/tweet.cpython-312.pyc b/api/models/__pycache__/tweet.cpython-312.pyc index 55741ec9a727ff3427ec9c5217320a724f40829e..30503ab44d80b8fd525b2348d3baa59680221afa 100644 GIT binary patch delta 3847 zcmbVPZBSI#8Qxv?i~Z!X?6TjxA_9wi?qZQ7#ux}hquVqI(}oG9E%%DFmNj=-Cpw)j zF)?a_ialCLBsE5fo2K!rCK8*5Wa_`pw40I6ER5r%Gu{QJKhzW^GoAkE`<@HSWrM`& zojK1vpYM6kx#xM#dq3aUzuELxqfsY=XVjC`TEqzvTn2C_Tn>)O5c zU`;SoSyNwGQ|s@I_!ml{THXW$=L`Vp!Tssc(e&{@O`RBtkvEmD%9t9eh4UW^*FK!X zZ2H80;rs`*cIL#GaO#Md9ZXD(Bx2;BN{iD7z4!tI8-g9diQpzC)k|_OIiPAP4`9Vw zfB?f6V}1=n3BaVLql1lfc69JstTK{0l`n+mCUd%bdPSQ#*dj5EMKs2jAyC`pq(yyQ z9v}wImvTM1r5TX>NJP6;?Z+Og$QRljYeJY^2N330V^%U4i_~B}3Fkf@&v+XS7_;2v zE3hwhmD;PsVif{f=BcMj%&8Evoy5A@1t@|{bR)oi*@Uua!sZ%kOtg*L*7p&!u0~CL zs3nZXp>WE{PjnR~E_b`a0o^fiIug!vFnUhS891$^_NLRp7{66(ayb~&w`%_;cWg6f z*RsY(z`jDpDOnS1{)6%Z1r*_F%i|bM$L5KxEi#sQRmSFLTJ!fP0v2*vSLqMalQq+K zXy)Ca+heEGFJIvGFn&_i8`;&}$y>?M!jhKem>25OLl=aB_ofg0UU)q*J$7>XbieQ# z9Jfz|^QY7Aj85u)8taX8{5-~Xah>SG!h^ey&%YldRc=q&u$on}xm^lY#p(dntR7Iq z8UT%~X;|GYZ_C+Z4&;$ddVAal{37&ta( zhBo2s`{^qq!fU^odGr1BC!fvXSsKCs%3YT7#LH5p!IZZ;Zp34PL8m+}S@4<|kKsa3&?;iSJY<*6U_ zCf!e_yhZn1CcEiwPM%$N$RM%}&vn3Ze`-+2Q9gLe1xgZ1h&ZJRT@s~A7O8Pvlggh( zB0HC*P77K@cnDJT9DqojlC_DZ(Vp>=ZGBH1TtCn>WEtulvL6e7sb76Vxox7TG@%@= z8ZX-1*L;xt%Mmd%D&5yI0C6oX$T4!n@VP#S=JBlZA@aQOqz})^WMOBdrLfk&lAb6+MlLQb&#e&kiQFl)$(uo4eDPros{=)mv4(f$tZ_JpRd>l* z4J0Kb6(otm{Kv8d#p)UE#TXaut!j+)Z13*kGm^vOv0Y?1@5kaQgbfIfBD{dmj__lI z4l=+fo92}OkJWN=u)Sq@|$YEP}IPWo$7p0!DL#l1Ba zq_{PU;`#pv#VNxVU6s=O2jIUO7qQ$4?!*R!U!cDi03Lwcq(9$DZaH=^-zaU!Y5elhBxcPlY#ZA8x4iN|g?0K>JJ7-FN{L1PFSUC)OPOP!jbCmwHJ z9?rXr4zib4J-69Cm3m;>M`Jfkb^gO)?x$}^ss3i2p zPdP#H{I(+MWMd}F&wfRU!4?y$#LuhouFoXna8>nrapONYq$XFfyrq z`G#2;yk?u^7AS0< ZmBDMaqEPGvui3gakl!;WgBSgR{{!#gRs;Y5 delta 2806 zcmbVOYiv|S6ux)gz5CvFA1%Afwx!VRW8Fuglt&v{9wF+2Jfbwp5_&IX6}ok{MVp8P zgaCm^acscYh>(C41gs(u{Q)HYiOGAt%?qYw*V>cuu2G5yGw+~A(++@BvGjnF< z>^a|=vxiq+Z!+C98U+r%OR3JS#l9iacM3j7ZzzUTUn_}`?od7mJ$8kI;Ye+;zBU;0 zbtQbGqezIDVPnn!kU6$5bM$2- zTKdGtvUqIp?&-lC{X)biaCORS3=?5dgoH)Sz685C9m~#$sZk zEfym>oHEgzdS=9d3(9y$7jfaF5}^dajo?MV!$=T;ogJntG(YnpYS8||8|ZJ^KE9MD zbgyX2QKN$1&~2?)h~i>^D5*wq+-4lI1Dj~Hpt(11ZPag^WG<;gU1lmfZ!Qk!A>hff zr7D!v2$PM(xrR|F10mT2u#dZ~D!c8NalB=)^_FA)E@~En8fHTSwQ7qMQI2jBYG!Xy z6oaxWM=n8g25eD zcO^RGZS<(yL04E@)KY2?;(VNMRrcup2D;PW>MaE|@*2Vtgl2>mgrx|wH&I%KupD3- zXRd|iMrcWl^Iq=uFBf_JjF$h0k@TM;S5P4{_&Gk8z<_pW$whR7J<@yM zZgv6ttdrSicmej|%~K{n{%(S>8861{8l|8k&%p^4q;EP(tdCSS(}{JWQ>+uL7&6{rs3v0u>29V z7DvXvi{@v0WzxIIxXJDr<0jrXtlFGNY@50kq^>0L)8NCD`ajo0X=E=lNKk zpw0v;VXeaNVVK5*YI=rG^#X;9MEf`jTO_Nb6CHWtI)S*ZJaKJ33V-pid3C%~+7|DS z)=2MmOUbTotV8Jwhco?`GC%CTJ9y}?i&rv3doqJ3(_b8l5K)s$EetXu)0 z4aGuwQ$JfvJSclf{aPrrP^4dyYiTFUO+x?z%LCAo3uvD@(3|p> z!+VXnB23H`f8n!9!Tf^JA!8L2t30VkK<`dj<N_63*su<8U|@tw`hAp?A|6dlASQ*p(VSi zX;%1UD=+kI$a2tSD^ZR=m7O3rWI5=vb%h|W%yQ6WYwWTPblG49$UQj@I(E~)X!I_( diff --git a/api/models/tweet.py b/api/models/tweet.py index 63d02a0..7a3f158 100644 --- a/api/models/tweet.py +++ b/api/models/tweet.py @@ -57,20 +57,29 @@ class TopicResponse(BaseModel): class ContentRequest(BaseModel): """内容生成请求模型""" - topic: Dict[str, Any] = Field(..., description="选题信息") + topic: Optional[Dict[str, Any]] = Field(None, description="选题信息") + styles: Optional[List[str]] = Field(None, description="风格列表") + audiences: Optional[List[str]] = Field(None, description="受众列表") + scenic_spots: Optional[List[str]] = Field(None, description="景区列表") + products: Optional[List[str]] = Field(None, description="产品列表") + auto_judge: bool = Field(False, description="是否自动进行内容审核") class Config: schema_extra = { "example": { "topic": { "index": "1", - "date": "2023-07-15", - "object": "北京故宫", - "product": "故宫门票", - "style": "旅游攻略", - "target_audience": "年轻人", - "logic": "暑期旅游热门景点推荐" - } + "date": "2024-07-01", + "style": "攻略风", + "target_audience": "亲子向", + "object": "天津冒险湾", + "product": "冒险湾-2大2小套票" + }, + "styles": ["攻略风", "种草风"], + "audiences": ["亲子向", "情侣向"], + "scenic_spots": ["天津冒险湾", "北京故宫"], + "products": ["冒险湾-2大2小套票", "故宫门票"], + "auto_judge": True } } @@ -97,8 +106,12 @@ class ContentResponse(BaseModel): class JudgeRequest(BaseModel): """内容审核请求模型""" - topic: Dict[str, Any] = Field(..., description="选题信息") + topic: Optional[Dict[str, Any]] = Field(None, description="选题信息") content: Dict[str, Any] = Field(..., description="要审核的内容") + styles: Optional[List[str]] = Field(None, description="风格列表") + audiences: Optional[List[str]] = Field(None, description="受众列表") + scenic_spots: Optional[List[str]] = Field(None, description="景区列表") + products: Optional[List[str]] = Field(None, description="产品列表") class Config: schema_extra = { @@ -116,7 +129,11 @@ class JudgeRequest(BaseModel): "title": "【北京故宫】避开人潮的秘密路线,90%的人都不知道!", "content": "故宫,作为中国最著名的文化遗产之一...", "tag": ["北京旅游", "故宫", "旅游攻略", "避暑胜地"] - } + }, + "styles": ["旅游攻略"], + "audiences": ["年轻人"], + "scenic_spots": ["北京故宫"], + "products": ["故宫门票"] } } @@ -144,25 +161,27 @@ class JudgeResponse(BaseModel): class PipelineRequest(BaseModel): - """完整流程请求模型""" - dates: Optional[str] = Field(None, description="日期字符串,可能为单个日期、多个日期用逗号分隔或范围如'2023-01-01 to 2023-01-31'") - num_topics: int = Field(5, description="要生成的选题数量", ge=1, le=10) + """流水线请求模型""" + dates: Optional[str] = Field(None, description="日期范围,如:'2024-07-01 to 2024-07-31'") + num_topics: int = Field(5, description="要生成的选题数量") styles: Optional[List[str]] = Field(None, description="风格列表") audiences: Optional[List[str]] = Field(None, description="受众列表") scenic_spots: Optional[List[str]] = Field(None, description="景区列表") products: Optional[List[str]] = Field(None, description="产品列表") skip_judge: bool = Field(False, description="是否跳过内容审核步骤") + auto_judge: bool = Field(False, description="是否在内容生成时进行内嵌审核") class Config: schema_extra = { "example": { - "dates": "2023-07-01 to 2023-07-31", + "dates": "2024-07-01 to 2024-07-31", "num_topics": 3, - "styles": ["旅游攻略", "亲子游"], - "audiences": ["年轻人", "家庭"], - "scenic_spots": ["故宫", "长城"], - "products": ["门票", "导游服务"], - "skip_judge": False + "styles": ["攻略风", "种草风"], + "audiences": ["亲子向", "情侣向"], + "scenic_spots": ["天津冒险湾", "北京故宫"], + "products": ["冒险湾-2大2小套票", "故宫门票"], + "skip_judge": False, + "auto_judge": True } } diff --git a/api/routers/__init__.py b/api/routers/__init__.py index 15d094d..5dce1b3 100644 --- a/api/routers/__init__.py +++ b/api/routers/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - + """ API路由模块 """ \ No newline at end of file diff --git a/api/routers/__pycache__/__init__.cpython-312.pyc b/api/routers/__pycache__/__init__.cpython-312.pyc index 7e8bd9cb39a97aaafe38b818cb6cf5bfd2c5aaa4..097640c80da334d125ca0bdbba7b16710bc44191 100644 GIT binary patch delta 21 bcmdnNxPy`RG%qg~0}u$@Dai1g$h#TFQAIeXRW)6)XOb3H^`6~05$}?{+ zAG=U~>HP91rGIJ}1K|mn?4Hgf`=V-EiG>Fr7{3_UF^1 zV-wkYI-AG9w#H_bf~?gK7mSvMA$&f!o6I20T5X|+NRk=BJy}4r%>6mVDPFU7FDr$6 zm%^#VaH?dH=R&g~_}bm(G;p1}*czwn6#w9KZQ`Q-_T%Er&IPGzftyxIXGPz7)eU~b z{n03UKJJJ|xPp_#!%2IrovLti(@+GUEv8Vn8-yt_5`0etR|ADVp!OzGw@SoZ{gbH4-fe$;KFm3Td zkE0a}a4bkX=>ZkXc2d1$`pDda=D~m_7pKDzWE~GkhU*5kTAPgN+in64-#FT9h=<8H z0#j~h*CQm!ypDVF3p5*uErd9^XDNALF?nDEamnVIOUa06_NSPw9QonT12 zSEcQJ%Y(d+NKU<#-0M}Z3OuF)?^c0F5V#aa-N_O$R){dt-9n;=FZ4zdVZIpFqAvFE zi5PgABO*2TbGX;eZNMFdo6Qc*VJ>5W$IW)82UC*-#M?@|T{etWm)hv{dB?4DMuunP zfYEj9JgytpqbGbdN3GFS2ZWsgIPLts zKjaGrH`{!3K+31j*TQR8D|7E}4$FJf{~Z=RoOA`XxfG@YAG`Kqx7ggFhJAQceK|9{ zYIMVxTdIGQe2-DhoTDVke1v=QJ2bVLwc6lacQRR$yh~DGQ3@z#u5Sb@B=li{gc#PR z8|$-Ueqdpu>flVilH@O0<&xb~a^F!Ev?fvEw1TKe7`k3ZT;>4S;q+yddjliV}^bMAlc zOn&#?_sd@wy+7)@#xVZ2pMU1_*jL`~b&&Z*&sO;ioCf}~*93(xc+GelnJBuMm3v94 zdDbe%CfD&_yjh=qs~&fc%QYYWFx0P>u%~4(_>ItjI}2768~izcCE~g4KR7WpVLe@& z*ni;ARBd9aW<~kGVUx>&VB0X-tP!3FL@SSyq_hXe^aq&!U^ix%6K+>V!R{?w>a8yI zR_$Q)SkGLKBeBiHu)hR*;+e-~+w5)N4>; zFq_V^k71kqNhHtCQR6)1**9u&Kx!y`#hld0DWnIa5?d?>A96vJza5^3$G8~?YooY| zCDLLcVgjTP-4LO3Av%L+jCOEyZ=x%H8<)V>RujzeWj3kvU5TvEN18(53-H-QKkVnf z8e<(m zv?mzWCh8Jdk=~{0&oHea?(h=g^5d7vV@u_+n~3dj{8)9a>L{!N2N1PX0c0(e0AUvd z?9`XZmd5-^+b|P$XO|t%+Odr-Rq<;MQ&i1aN9NU52kpZ49DWUVL5JCUiYhw`MYsHg zfvV9)&4wFDt7=J;8o|DjA~h8SbyJf{I=5qenu_ngj$@^c;6LT(;~fOweH(s2zz?EB z#7+3&TlJ(N;M1`bA>hXa{8RkfM0!JgxqkS{TW7Amck;@Sqt|BMymspN?=!EimCJ7& zUOszndA>fM%r3sDNx=V1Zc(#tYdfxG<@ryY2A@f_^SRV0c=^>-4?M|Z>D;!xH2oB% z|KE>&G)3t&j_He-*7hS4;`Ma7;P}{%JxJcO1HpUxk^2_p{_ye2!l71}`7_Af4n^%q z(oWoa2lfbBS6dk#%k11L!)(#S%h}if#e&?p%OE`fgEBMRj)xJ_*YUUNx z-+?qJ^N%vu{o+E36EDuqK7PVXYR}?N0{!g|{DS#&cLea@=y69P@mnbUmqKDiVy+Qz zT%?`*`Gxi4-JK|HJ?cm#gpmXphtNpcd5{lvnH$%`ZZswwiG(ncAmd)R?jmjV;P?Ln D=Jhp~ diff --git a/api/routers/tweet.py b/api/routers/tweet.py index 7eb282d..180680a 100644 --- a/api/routers/tweet.py +++ b/api/routers/tweet.py @@ -92,13 +92,23 @@ async def generate_content( tweet_service: TweetService = Depends(get_tweet_service) ): """ - 为选题生成内容 + 生成内容 - **topic**: 选题信息 + - **styles**: 风格列表 + - **audiences**: 受众列表 + - **scenic_spots**: 景区列表 + - **products**: 产品列表 + - **auto_judge**: 是否自动进行内容审核 """ try: request_id, topic_index, content = await tweet_service.generate_content( - topic=request.topic + topic=request.topic, + styles=request.styles, + audiences=request.audiences, + scenic_spots=request.scenic_spots, + products=request.products, + auto_judge=request.auto_judge ) return ContentResponse( @@ -150,11 +160,19 @@ async def judge_content( - **topic**: 选题信息 - **content**: 要审核的内容 + - **styles**: 风格列表 + - **audiences**: 受众列表 + - **scenic_spots**: 景区列表 + - **products**: 产品列表 """ try: request_id, topic_index, judged_content, judge_success = await tweet_service.judge_content( topic=request.topic, - content=request.content + content=request.content, + styles=request.styles, + audiences=request.audiences, + scenic_spots=request.scenic_spots, + products=request.products ) return JudgeResponse( @@ -174,15 +192,16 @@ async def run_pipeline( tweet_service: TweetService = Depends(get_tweet_service) ): """ - 运行完整流水线,包括生成选题、生成内容和审核内容 + 运行完整流水线:生成选题 → 生成内容 → 审核内容 - - **dates**: 日期字符串,可能为单个日期、多个日期用逗号分隔或范围 + - **dates**: 日期范围 - **num_topics**: 要生成的选题数量 - **styles**: 风格列表 - **audiences**: 受众列表 - **scenic_spots**: 景区列表 - **products**: 产品列表 - **skip_judge**: 是否跳过内容审核步骤 + - **auto_judge**: 是否在内容生成时进行内嵌审核 """ try: request_id, topics, contents, judged_contents = await tweet_service.run_pipeline( @@ -192,7 +211,8 @@ async def run_pipeline( audiences=request.audiences, scenic_spots=request.scenic_spots, products=request.products, - skip_judge=request.skip_judge + skip_judge=request.skip_judge, + auto_judge=request.auto_judge ) return PipelineResponse( diff --git a/api/services/__pycache__/prompt_builder.cpython-312.pyc b/api/services/__pycache__/prompt_builder.cpython-312.pyc index 750111f3fe660edefe9b253b9dea2b5916765509..67c8817f5c1e44da077427e887625f5e1ade6782 100644 GIT binary patch delta 4307 zcmcgvdr(u^8Nc_EguD|-LJ}Z6L=6|@5rVF-q9UO9fcix6!9wmuqY&s#C~7Z@Zf(nU z+}iEgI!m{!Y^Sq}kEuB9v_b6%olaM$Go71Zrl$F$vpW@CJJU5Q?H}%6{m#jQNZE1w z&tAxnbH04{`_AL{o!}O~JG5?|sKkY`dwSrkOSdh3I-Z8(Ep+{VFQ) z!{u;lUdQWsLl_&t;yIx0%rfrtF02oUF~l z<#H)LB<8{xFUx6$9AreCh0o&Tyb;E$kBzrW&t*YAE6!)ILOxq^W)849_Ps3j!m%tI z>%wd{$Y#gcG%4|8}4MGb02?Yk70hCbC;(It(U)}oNzlA*mu#{+15RU&=d zfL!S&lZ>O2fpww|-eVH)p((A(_%8Kc+Mot9>czA~M)U)lm5zmfzTAX9wiU_e1IkkZ z8bx?N9uPdUMPp(NB&;67W{9$6*f;Hou-CO_B-xjwgjJ-3HHoHVSn#x)UWl3jQO%+} z5!EKjVRq6YH`5lfJeEX|L37ITq^G3K60?$N;XS0!(J_N&vRdkFlTlP8 zBUnZCaXZ?Sn-7nlxW5*|8Vu_&tjF*z z3^f=wVAu%2W(#KAF2=Bf8Uoyjn@t#+F|ZgmVW`Eh8ABZgTyL=f*5ei_G-aY>ga&LQ zr6$8gBW%HaQfFimTXA<4hDHqA0Q}h$MN{bg!YnjiC}RZ+*dhg;{oZb_-P7v}@V-Dh z6kbnXpnbp_IM&|RA$0WkPp^c7AjkRD4t+TEZK%96RI~(rZd`}1E@^N%mRxQfX&oIH zXJ#Dk;ihnYnUq&P);Vr|;HnF`>K?fEhg|!m7JkOnB~>1ka*rWb(W}*YMVGxJUa5FZ zuxci+X1F*;%P|*^qXps(hW-2(ToR=J3=+x4ts+@w$Yev?L zw!#>5+`~JJ7-)TsgjfO_~B;saYl7Vwlkc=;@{lTaJDn5p{%Ys zN?}zGYobQV=y>OZWMCyF`}|oeO*seX=g*8(@fL>sd5!egjf~$0t2V84Hma3>Snq5! zDF0aLY&0wXwA9&XS56$ryv1l*z$3P+)yQFKun?{h>JhHR7=}(-mgXVB!+Di zy1=$FTj+^@vMSoPxS%T1zMh~iP>gRtP!;nUr2vgGq-c~uG$b|Z|I%2H+=L9pG?wVo zb*)Ly&FfQ!s79ZH#Fjf%x=b#eQ+8_xK~zDrK&dPYr{dJtHP`TU1RfC6xQ6XAi$uk) zYp==T-&w>=(UPiQ0|_lNfQsq*hhCy$SDok3PaO4e6)QsJ#k+@+B1URCMY{w@D!^Gx zH8lu++zwzE07xj>DQq~60hfGAt&>xQaP>uo3WQ7|cHRiUW(j6cvACiUe$LnJys9n6 z*<=yEM66QT3)+$e*%B%r`-^TA>I?H?%5}V-JIXJnTW-)Tr#)55PQpt2KLP-yDn|cd z*P&-c4d0k_43>On)$?Z?sN#7M(8A0>1nSpd5vZ?Gdg6QnGes%jgZp!%6VuC<(6j>W zit$mS;bfe=TZJ#Nz|kxtr{L1z%9J3Z4$-=-b)pV##A=SYu1YddkGYAN$g2CZQ(5=u z(z)5&w;uoI%DFI;`{%QK&SaQ5M?4`k{3x;5W5iB7onBk+tb(0pDxFY(zVBWlbB4i=yMjKk&<-&EYJ%L9d(sSp<*q1BV=sxSsakpqJ>|c*dFteJ{v1gjJ;F?suUJhF%~2H)mZPaUdMh1n z*cNWw8Ln^qLbU+~SQj(}Jwe;erYHs7dwZqb`=pjOX?NTGBg7IeTZt!e(;F(=7)=A0 zPbyaj%|XwkJyf|lDhC6Mw|afBZpt*-2=~EgIyNaO@JdY76UCXp)=clErM)du>mh0H zq5Hja#9Yg)ke7@F#y1B|<6E!17NwwjPj+t)4CLN{`+Cxs4!$aNa8j37>hOjR9{)G? z5v+JGi~y`v_#rw~c8UEyw&W(v!W+0nyvV&;xxNSf|Lx{$gtySf@;bw=bh_jjg^@V@ E7dK;M761SM delta 1117 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 Yf7sv39m9s&9i}H0RLfV0P)X?j0AW`eQUCw| delta 2585 zcmZ`)T~Hg>6~1?+m3FmS{Y!uVBLPAdV1A4Z7=)>e4JH_j4NlCDA+B~Il5L9Sv-1Y^U#-=|HtKim*+DY3yv`HUYIqpn7?M$clE|P;Y z_1)QT&pG$pd(S=hyJxSy{>7`NU-P^H;FjK=d9Bv}fvJ_^3W$!|lm9|ANY%o8G^#y` zwkV%TFpIp%ilzyQ_@FUOVX;=QT-Od;p6?N?A}iQLmUwhCf*pHUzvAK8J(IwTMm#UO z+a}s3v}BbDo#4dd?JPiZ)QH9<+R$BX4`&hbuG7Pg%esJjjiK*yo-jpX&Ne;%Uc>K` zpF#k700i#<#P%9buj3+c<8?jW~VdT0pfW%DVmR_9A%tN zFu1{O@SZlQ!*P_}oB~R^ zx-14ywx#LFlpxNb(!4rDS|6UCii=b6K6E30falYiNiilOnp3{! zN50;auXj^a&WvsO##2n`Jiqt`YVm&8Qq@riH}=7f0_{dksJf$YEmZs~O4mu2=t5Rc zed}>7_DL@h!#)Bv1ZoNN5IBJ$z)8JC-U*#ZZUQG!($lQ#CnAZ~4Ns!qd1_!1(ca&e zcVlxndstWmfWfcIh0k8h1&zuD4d%jO+jC6NCh0Gk;G<+HYh`*xt4@q(73k-(NPMd(@yL}xErA;y--Q2;+3`GY zV;1d)8#S<_PP z=vL(+vrf?G(kz3h75y2X-`^@-?&t2gS{K-)K{mt+Mb1H_CjjU(JCzX+i&}uJZG;Q8vq#U3Xz8Uz%)Pw8~c5v|vY} zeSW|F1reKe$hrt`HsbR37j$c%lefil!GdUj+@&_N8jVG^k{YiZ{^+&K4*l&g}sg>vycEqJNE?$bIwNv6;JguKSFHJ?JCZ%HB zCB005q+5E0c(TXl!s1LkGCh@M!crt237@|haG>{1Wt23Ge$Y?~yU|AtCDyaV8Hy6A zX)y-|^t9nf)hf|E_=ogVy+i-31g!ZMx4J?X}qsK+yeSO=|WcS3^>!w zI4WwI$dgEtrIQ%a+OJPfUzCQ39!2jq?V)Z{=qF9T*q=w&EgfC8uZCCim-{jRtM?M? z1Dj==ab@roWoT3x9R1}L<=DAY``8oGo7s>2hr(|b&>e8eu@qiySbA-FPX-Wuy?i~k z*|9#iQM02fXNHxLG3C{;B1V+xC1qMtqEbqXDT&L<$mP_T*Pp1vGL7IAJPo&-TDMyc zZ?}cE54CMq*FWZhblVcMYG39v0E?St>Sx; Tuple[str, str]: + """ + 使用额外参数构建内容生成提示词 + + Args: + topic: 选题信息 + styles: 风格列表 + audiences: 受众列表 + scenic_spots: 景区列表 + products: 产品列表 + step: 当前步骤,用于过滤参考内容 + + Returns: + 系统提示词和用户提示词的元组 + """ + # 获取内容生成配置 + content_config = self._ensure_content_config() + + # 加载系统提示词和用户提示词模板 + system_prompt_path = content_config.content_system_prompt + user_prompt_path = content_config.content_user_prompt + + # 创建提示词模板 + template = PromptTemplate(system_prompt_path, user_prompt_path) + + # 获取风格内容 + style_content = '' + if styles: + style_content = '\n'.join([f"{style}: {self.prompt_service.get_style_content(style)}" for style in styles]) + else: + style_filename = topic.get("style", "") + style_content = f"{style_filename}\n{self.prompt_service.get_style_content(style_filename)}" + + # 获取目标受众内容 + demand_content = '' + if audiences: + demand_content = '\n'.join([f"{audience}: {self.prompt_service.get_audience_content(audience)}" for audience in audiences]) + else: + demand_filename = topic.get("target_audience", "") + demand_content = f"{demand_filename}\n{self.prompt_service.get_audience_content(demand_filename)}" + + # 获取景区信息 + object_content = '' + if scenic_spots: + object_content = '\n'.join([f"{spot}: {self.prompt_service.get_scenic_spot_info(spot)}" for spot in scenic_spots]) + else: + object_name = topic.get("object", "") + object_content = f"{object_name}\n{self.prompt_service.get_scenic_spot_info(object_name)}" + + # 获取产品信息 + product_content = '' + if products: + product_content = '\n'.join([f"{product}: {self.prompt_service.get_product_info(product)}" for product in products]) + else: + product_name = topic.get("product", "") + product_content = f"{product_name}\n{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=style_content, + demand_content=demand_content, + object_content=object_content, + product_content=product_content, + refer_content=refer_content + ) + + return system_prompt, user_prompt + def build_poster_prompt(self, topic: Dict[str, Any], content: Dict[str, Any]) -> Tuple[str, str]: """ 构建海报生成提示词 @@ -292,4 +371,115 @@ class PromptBuilderService: refer_content=refer_content ) + return system_prompt, user_prompt + + def build_judge_prompt_with_params(self, topic: Dict[str, Any], content: Dict[str, Any], + styles: Optional[List[str]] = None, + audiences: Optional[List[str]] = None, + scenic_spots: Optional[List[str]] = None, + products: Optional[List[str]] = None) -> Tuple[str, str]: + """ + 使用额外参数构建内容审核提示词 + + Args: + topic: 选题信息 + content: 生成的内容 + styles: 风格列表 + audiences: 受众列表 + scenic_spots: 景区列表 + products: 产品列表 + + Returns: + 系统提示词和用户提示词的元组 + """ + # 获取内容生成配置 + content_config = self._ensure_content_config() + + # 从配置中获取审核提示词模板路径 + system_prompt_path = content_config.judger_system_prompt + user_prompt_path = content_config.judger_user_prompt + + # 创建提示词模板 + template = PromptTemplate(system_prompt_path, user_prompt_path) + + # 获取景区信息 + object_content = '' + if scenic_spots: + object_content = '\n'.join([f"{spot}: {self.prompt_service.get_scenic_spot_info(spot)}" for spot in scenic_spots]) + else: + object_name = topic.get("object", "") + object_content = f"{object_name}\n{self.prompt_service.get_scenic_spot_info(object_name)}" + + # 获取产品信息 + product_content = '' + if products: + product_content = '\n'.join([f"{product}: {self.prompt_service.get_product_info(product)}" for product in products]) + else: + product_name = topic.get("product", "") + product_content = f"{product_name}\n{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 + + def build_judge_prompt_simple(self, topic: Dict[str, Any], content: Dict[str, Any]) -> Tuple[str, str]: + """ + 构建简化的内容审核提示词(只需要产品信息、景区信息和文章) + + Args: + topic: 选题信息 + content: 生成的内容 + + Returns: + 系统提示词和用户提示词的元组 + """ + # 获取内容生成配置 + content_config = self._ensure_content_config() + + # 从配置中获取审核提示词模板路径 + system_prompt_path = content_config.judger_system_prompt + user_prompt_path = 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) + + # 构建系统提示词 + 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="" # 简化版不使用参考内容 + ) + return system_prompt, user_prompt \ No newline at end of file diff --git a/api/services/tweet.py b/api/services/tweet.py index 12bf79e..0e17e23 100644 --- a/api/services/tweet.py +++ b/api/services/tweet.py @@ -97,24 +97,68 @@ class TweetService: logger.info(f"选题生成完成,请求ID: {request_id}, 数量: {len(topics)}") return request_id, topics - async def generate_content(self, topic: Dict[str, Any]) -> Tuple[str, str, Dict[str, Any]]: + async def generate_content(self, topic: Optional[Dict[str, Any]] = None, + styles: Optional[List[str]] = None, + audiences: Optional[List[str]] = None, + scenic_spots: Optional[List[str]] = None, + products: Optional[List[str]] = None, + auto_judge: bool = False) -> Tuple[str, str, Dict[str, Any]]: """ 为选题生成内容 Args: topic: 选题信息 + styles: 风格列表 + audiences: 受众列表 + scenic_spots: 景区列表 + products: 产品列表 + auto_judge: 是否自动进行内容审核 Returns: - 请求ID、选题索引和生成的内容 + 请求ID、选题索引和生成的内容(如果启用审核则返回审核后的内容) """ + # 如果没有提供topic,创建一个基础的topic + if not topic: + topic = {"index": "1", "date": "2024-07-01"} + topic_index = topic.get('index', 'unknown') - logger.info(f"开始为选题 {topic_index} 生成内容") + logger.info(f"开始为选题 {topic_index} 生成内容{'(含审核)' if auto_judge else ''}") + + # 创建topic的副本并应用覆盖参数 + enhanced_topic = topic.copy() + if styles and len(styles) > 0: + enhanced_topic['style'] = styles[0] # 使用第一个风格 + if audiences and len(audiences) > 0: + enhanced_topic['target_audience'] = audiences[0] # 使用第一个受众 + if scenic_spots and len(scenic_spots) > 0: + enhanced_topic['object'] = scenic_spots[0] # 使用第一个景区 + if products and len(products) > 0: + enhanced_topic['product'] = products[0] # 使用第一个产品 # 使用PromptBuilderService构建提示词 - system_prompt, user_prompt = self.prompt_builder.build_content_prompt(topic, "content") + system_prompt, user_prompt = self.prompt_builder.build_content_prompt(enhanced_topic, "content") # 使用预构建的提示词生成内容 - content = await self.content_generator.generate_content_with_prompt(topic, system_prompt, user_prompt) + content = await self.content_generator.generate_content_with_prompt(enhanced_topic, system_prompt, user_prompt) + + # 如果启用自动审核,则进行内嵌审核 + if auto_judge: + logger.info(f"开始对选题 {topic_index} 的内容进行内嵌审核") + try: + # 构建简化的审核提示词(只需要产品信息、景区信息和文章) + judge_system_prompt, judge_user_prompt = self.prompt_builder.build_judge_prompt_simple(enhanced_topic, content) + + # 进行审核 + judged_content = await self.content_judger.judge_content_with_prompt(content, enhanced_topic, judge_system_prompt, judge_user_prompt) + + if judged_content.get('judge_success', False): + logger.info(f"选题 {topic_index} 内容审核成功,使用审核后的内容") + content = judged_content + else: + logger.warning(f"选题 {topic_index} 内容审核失败,使用原始内容") + + except Exception as e: + logger.error(f"选题 {topic_index} 内嵌审核失败: {e},使用原始内容") # 生成请求ID request_id = f"content_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{str(uuid.uuid4())[:8]}" @@ -146,25 +190,52 @@ class TweetService: logger.info(f"内容生成完成,请求ID: {request_id}, 选题索引: {topic_index}") return request_id, topic_index, content - async def judge_content(self, topic: Dict[str, Any], content: Dict[str, Any]) -> Tuple[str, str, Dict[str, Any], bool]: + async def judge_content(self, topic: Optional[Dict[str, Any]] = None, content: Dict[str, Any] = {}, + styles: Optional[List[str]] = None, + audiences: Optional[List[str]] = None, + scenic_spots: Optional[List[str]] = None, + products: Optional[List[str]] = None) -> Tuple[str, str, Dict[str, Any], bool]: """ 审核内容 Args: topic: 选题信息 content: 要审核的内容 + styles: 风格列表 + audiences: 受众列表 + scenic_spots: 景区列表 + products: 产品列表 Returns: 请求ID、选题索引、审核后的内容和审核是否成功 """ + # 如果没有提供topic,创建一个基础的topic + if not topic: + topic = {"index": "1", "date": "2024-07-01"} + + # 如果没有提供content,返回错误 + if not content: + content = {"title": "未提供内容", "content": "未提供内容"} + topic_index = topic.get('index', 'unknown') logger.info(f"开始审核选题 {topic_index} 的内容") + # 创建topic的副本并应用覆盖参数 + enhanced_topic = topic.copy() + if styles and len(styles) > 0: + enhanced_topic['style'] = styles[0] # 使用第一个风格 + if audiences and len(audiences) > 0: + enhanced_topic['target_audience'] = audiences[0] # 使用第一个受众 + if scenic_spots and len(scenic_spots) > 0: + enhanced_topic['object'] = scenic_spots[0] # 使用第一个景区 + if products and len(products) > 0: + enhanced_topic['product'] = products[0] # 使用第一个产品 + # 使用PromptBuilderService构建提示词 - system_prompt, user_prompt = self.prompt_builder.build_judge_prompt(topic, content) + system_prompt, user_prompt = self.prompt_builder.build_judge_prompt(enhanced_topic, content) # 审核内容 - judged_data = await self.content_judger.judge_content_with_prompt(content, topic, system_prompt, user_prompt) + judged_data = await self.content_judger.judge_content_with_prompt(content, enhanced_topic, system_prompt, user_prompt) judge_success = judged_data.get('judge_success', False) # 生成请求ID @@ -178,7 +249,8 @@ class TweetService: audiences: Optional[List[str]] = None, scenic_spots: Optional[List[str]] = None, products: Optional[List[str]] = None, - skip_judge: bool = False) -> Tuple[str, List[Dict[str, Any]], Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]]]: + skip_judge: bool = False, + auto_judge: bool = False) -> Tuple[str, List[Dict[str, Any]], Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]]]: """ 运行完整流水线 @@ -189,12 +261,13 @@ class TweetService: audiences: 受众列表 scenic_spots: 景区列表 products: 产品列表 - skip_judge: 是否跳过内容审核步骤 + skip_judge: 是否跳过内容审核步骤(与auto_judge互斥) + auto_judge: 是否在内容生成时进行内嵌审核 Returns: 请求ID、生成的选题列表、生成的内容和审核后的内容 """ - logger.info(f"开始运行完整流水线,日期: {dates}, 数量: {num_topics}") + logger.info(f"开始运行完整流水线,日期: {dates}, 数量: {num_topics}, 内嵌审核: {auto_judge}") # 生成请求ID request_id = f"pipeline_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{str(uuid.uuid4())[:8]}" @@ -205,19 +278,19 @@ class TweetService: logger.error("未能生成任何选题,流程终止") return request_id, [], {}, {} - # 步骤2: 为每个选题生成内容 + # 步骤2: 为每个选题生成内容(可选择内嵌审核) contents = {} for topic in topics: topic_index = topic.get('index', 'unknown') - _, _, content = await self.generate_content(topic) + _, _, content = await self.generate_content(topic, auto_judge=auto_judge) contents[topic_index] = content - # 如果跳过审核,直接返回结果 - if skip_judge: - logger.info(f"跳过内容审核步骤,流水线完成,请求ID: {request_id}") + # 如果使用内嵌审核或跳过审核,直接返回结果 + if auto_judge or skip_judge: + logger.info(f"{'使用内嵌审核' if auto_judge else '跳过内容审核步骤'},流水线完成,请求ID: {request_id}") return request_id, topics, contents, contents - # 步骤3: 审核内容 + # 步骤3: 独立审核内容(仅在未使用内嵌审核且未跳过审核时执行) judged_contents = {} for topic_index, content in contents.items(): topic = next((t for t in topics if t.get('index') == topic_index), None) diff --git a/core/config/__pycache__/manager.cpython-312.pyc b/core/config/__pycache__/manager.cpython-312.pyc index e3edd69f0a8e4d7eac134923d2ecaca52be38e6f..5b3d31c1e438a75dbe5cb8e18a2480c98922760e 100644 GIT binary patch delta 33 ncmey9(2>Y{nwOW00SM~v6l4f) None: + """ + 注册一个配置类 + + Args: + name: 配置名称 + config_class: 配置类 (必须是 BaseConfig 的子类) + """ + if not issubclass(config_class, BaseConfig): + raise TypeError("config_class must be a subclass of BaseConfig") + if name not in self._configs: + self._configs[name] = config_class() + + def get_config(self, name: str, config_class: Type[T]) -> T: + """ + 获取配置实例 + + Args: + name: 配置名称 + config_class: 配置类 (用于类型提示) + + Returns: + 配置实例 + """ + config = self._configs.get(name) + if config is None: + # 如果配置不存在,先注册一个默认实例 + self.register_config(name, config_class) + config = self._configs.get(name) + + # 确保配置是正确的类型 + if not isinstance(config, config_class): + # 尝试转换配置 + try: + if isinstance(config, BaseConfig): + # 将现有配置转换为请求的类型 + new_config = config_class(**config.model_dump()) + self._configs[name] = new_config + config = new_config + else: + raise TypeError(f"Configuration '{name}' is not of type '{config_class.__name__}'") + except Exception as e: + logger.error(f"转换配置 '{name}' 到类型 '{config_class.__name__}' 失败: {e}") + raise TypeError(f"Configuration '{name}' is not of type '{config_class.__name__}'") from e + + return cast(T, config) + + def get_raw_config(self, name: str) -> Dict[str, Any]: + """ + 获取原始配置数据 + + Args: + name: 配置名称 + + Returns: + 原始配置数据字典 + """ + if name in self._raw_configs: + return self._raw_configs[name] + + # 如果没有原始配置,但有对象配置,则转换为字典 + if name in self._configs: + return self._configs[name].to_dict() + + # 尝试从文件加载 + if self.config_dir: + config_path = self.config_dir / f"{name}.json" + if config_path.exists(): + try: + with open(config_path, 'r', encoding='utf-8') as f: + raw_config = json.load(f) + self._raw_configs[name] = raw_config + return raw_config + except Exception as e: + logger.error(f"加载原始配置 '{name}' 失败: {e}") + + # 返回空字典 + return {} + + def _load_all_configs_from_dir(self, server_mode: bool = False): + """ + 动态加载目录中的所有.json文件 + + Args: + server_mode: 是否为服务器模式,如果是则只加载必要的全局配置 + """ + try: + # 遍历并加载目录中所有其他的 .json 文件 + for config_path in self.config_dir.glob('*.json'): + config_name = config_path.stem # 'topic_gen.json' -> 'topic_gen' + + # 服务器模式下,只加载必要的全局配置 + if server_mode and config_name not in self.SERVER_CONFIGS: + logger.info(f"服务器模式下跳过非全局配置: {config_name}") + continue + + # 加载原始配置 + with open(config_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + self._raw_configs[config_name] = config_data + + # 更新对象配置 + if config_name in self._configs: + logger.info(f"加载配置文件 '{config_name}': {config_path}") + self._configs[config_name].update(config_data) + self._loaded_configs.add(config_name) + else: + logger.info(f"加载原始配置 '{config_name}': {config_path}") + + # 最后应用环境变量覆盖 + self._apply_env_overrides() + + except Exception as e: + logger.error(f"从目录 '{self.config_dir}' 加载配置失败: {e}", exc_info=True) + raise + + def load_task_config(self, config_name: str) -> bool: + """ + 按需加载任务配置 + + Args: + config_name: 配置名称 + + Returns: + 是否成功加载 + """ + if config_name in self._loaded_configs: + return True + + if self.config_dir: + config_path = self.config_dir / f"{config_name}.json" + if config_path.exists(): + try: + with open(config_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + self._raw_configs[config_name] = config_data + + if config_name in self._configs: + self._configs[config_name].update(config_data) + self._loaded_configs.add(config_name) + logger.info(f"按需加载任务配置 '{config_name}': {config_path}") + return True + except Exception as e: + logger.error(f"加载任务配置 '{config_name}' 失败: {e}") + + logger.warning(f"未找到任务配置: {config_name}") + return False + + def _apply_env_overrides(self): + """应用环境变量覆盖""" + logger.info("应用环境变量覆盖...") + # 示例: AI模型配置环境变量覆盖 + ai_model_config = self.get_config('ai_model', AIModelConfig) + if not ai_model_config: return # 如果没有AI配置则跳过 + + env_mapping = { + 'AI_MODEL': 'model', + 'API_URL': 'api_url', + 'API_KEY': 'api_key' + } + update_data = {} + for env_var, config_key in env_mapping.items(): + if os.getenv(env_var): + update_data[config_key] = os.getenv(env_var) + + if update_data: + ai_model_config.update(update_data) + # 更新原始配置 + if 'ai_model' in self._raw_configs: + for key, value in update_data.items(): + self._raw_configs['ai_model'][key] = value + logger.info(f"通过环境变量更新了AI模型配置: {list(update_data.keys())}") + + def save_config(self, name: str): + """ + 保存指定的配置到文件 + + Args: + name: 要保存的配置名称 + """ + if not self.config_dir: + raise ValueError("配置目录未设置,无法保存文件") + + path = self.config_dir / f"{name}.json" + config = self.get_config(name, BaseConfig) + config_data = config.to_dict() + + # 更新原始配置 + self._raw_configs[name] = config_data + + try: + with open(path, 'w', encoding='utf-8') as f: + json.dump(config_data, f, indent=4, ensure_ascii=False) + logger.info(f"配置 '{name}' 已保存到 {path}") + except Exception as e: + logger.error(f"保存配置 '{name}' 到 {path} 失败: {e}", exc_info=True) + raise + + +# 全局配置管理器实例 +config_manager = ConfigManager() + +def get_config_manager() -> ConfigManager: + return config_manager \ No newline at end of file