73 lines
9.5 KiB
Python
73 lines
9.5 KiB
Python
"""Create reproducible, importable Dify canvases from the application's prompts."""
|
|||
|
|
import json
|
||
|
|
import argparse
|
||
|
|
import importlib.util
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument('--provider-id', help='Native model tool provider ID created by dify-model-tools.py')
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
DEST = ROOT / 'deploy/dify/workflows'
|
||
|
|
DEST.mkdir(parents=True, exist_ok=True)
|
||
|
|
events = (ROOT / 'internal/extraction/events.go').read_text(encoding='utf-8')
|
||
|
|
memoir = (ROOT / 'internal/memoir/generation.go').read_text(encoding='utf-8')
|
||
|
|
def constant(source, name): return source.split('const '+name+' = `', 1)[1].split('`', 1)[0]
|
||
|
|
extract_prompt = constant(events, 'eventExtractionPrompt')
|
||
|
|
daily_prompt = constant(events, 'dailyExtractionPrompt')
|
||
|
|
book_prompt = constant(memoir, 'MemoirPrompt')
|
||
|
|
|
||
|
|
def node(id, type, title, x, y, **data):
|
||
|
|
return dict(id=id, type='custom', position=dict(x=x,y=y), positionAbsolute=dict(x=x,y=y), width=244, height=110, sourcePosition='right', targetPosition='left', data=dict(type=type,title=title,desc='',selected=False,**data))
|
||
|
|
def edge(a,b,nodes,iteration=None):
|
||
|
|
types={n['id']:n['data']['type'] for n in nodes}
|
||
|
|
data=dict(sourceType=types[a],targetType=types[b],isInIteration=bool(iteration),isInLoop=False)
|
||
|
|
if iteration: data['iteration_id']=iteration
|
||
|
|
return dict(id=a+'-'+b,source=a,target=b,sourceHandle='source',targetHandle='target',type='custom',zIndex=1002 if iteration else 0,data=data)
|
||
|
|
def start():
|
||
|
|
return node('start','start','开始:接收后台任务',50,80,variables=[dict(variable=v,label=l,type='text-input',required=False,max_length=128,options=[],default='') for v,l in [('context_id','任务上下文(后台自动传入;留空可测试示例)'),('step_token','临时执行凭证(留空可测试示例)')]])
|
||
|
|
def http(id,title,step,x,y,kind=None,prompt=None,prepared=True,index=None):
|
||
|
|
selector='prepared' if prepared else 'start'
|
||
|
|
body={'context_id':'{{#'+selector+'.context_id#}}','step_token':'{{#'+selector+'.step_token#}}'}
|
||
|
|
if kind:body['kind']=kind
|
||
|
|
if prompt:body['prompt']=prompt
|
||
|
|
text=json.dumps(body,ensure_ascii=False)
|
||
|
|
if index is not None:text=text[:-1]+',"chapter_index":{{#pictures.item#}}}'
|
||
|
|
return node(id,'http-request',title,x,y,method='POST',url='http://model-bridge:8188/steps/'+step,authorization={'type':'no-auth'},headers='Content-Type:application/json',params='',body={'type':'json','data':[{'id':'body','key':'','type':'text','value':text}]},timeout={'connect':15,'read':240,'write':30},retry_config={'enabled':False,'max_retries':1,'retry_interval':1000})
|
||
|
|
def parse(id,title,source,fields,x,y,allow_errors=False):
|
||
|
|
code='import json\ndef main(body: str, status_code: int) -> dict:\n if status_code != 200: raise Exception("HTTP_STEP_FAILED_"+str(status_code))\n data=json.loads(body)\n'
|
||
|
|
if not allow_errors:code+=' if not data.get("success"): raise Exception(data.get("error_code","MODEL_STEP_FAILED"))\n'
|
||
|
|
code+=' return {'+','.join(repr(k)+': data.get('+repr(k)+','+('""' if typ=='string' else '[]' if typ.startswith('array') else '0')+')' for k,typ in fields.items())+'}\n'
|
||
|
|
return node(id,'code',title,x,y,code_language='python3',code=code,variables=[{'variable':'body','value_selector':[source,'body']},{'variable':'status_code','value_selector':[source,'status_code']}],outputs={k:{'type':t,'children':None} for k,t in fields.items()})
|
||
|
|
def end(source,fields,x,y):
|
||
|
|
return node('end','end','完成:由后台核验并保存',x,y,outputs=[dict(variable=k,value_type=t,value_selector=[source,k]) for k,t in fields.items()])
|
||
|
|
def app(name,description,nodes,edges):
|
||
|
|
return dict(kind='app',version='0.7.0',dependencies=[],app=dict(name=name,description=description,mode='workflow',icon='📖',icon_background='#ECFDF3',use_icon_as_answer_icon=False),workflow=dict(conversation_variables=[],environment_variables=[],features={'file_upload':{'enabled':False},'retriever_resource':{'enabled':False},'sensitive_word_avoidance':{'enabled':False},'speech_to_text':{'enabled':False},'text_to_speech':{'enabled':False},'suggested_questions_after_answer':{'enabled':False}},graph=dict(nodes=nodes,edges=edges,viewport=dict(x=0,y=0,zoom=.7))))
|
||
|
|
|
||
|
|
nodes=[start(),http('prepare','读取聊天与已有故事','prepare',360,80,kind='extract',prepared=False),parse('prepared','接收任务 / 示例测试','prepare',{'context_id':'string','step_token':'string'},670,80),http('classify','DeepSeek:分类与故事拆分','classify',670,300,prompt=extract_prompt),parse('classified','检查故事拆分结果','classify',{'story_count':'number','daily_candidate_count':'number'},360,300),http('daily','DeepSeek:只审核今天实际事件','daily',50,300,prompt=daily_prompt),parse('today','检查当日原话证据','daily',{'today_actual_count':'number'},50,520),http('dedup','去重:同事件比较新增信息','dedup',360,520),parse('merged','输出新增 / 更新 / 重复计划','dedup',{'new_stories':'number','updated_stories':'number','repeated_stories':'number','daily_count':'number'},670,520)]
|
||
|
|
fields={'new_stories':'number','updated_stories':'number','repeated_stories':'number','daily_count':'number'};nodes.append(end('merged',fields,670,740));edges=[edge(a['id'],b['id'],nodes) for a,b in zip(nodes,nodes[1:])]
|
||
|
|
extract=app('聊天整理:当日日常与回忆故事','DeepSeek 分类与事件拆分 → 独立审核今天已发生事项 → 同故事去重与新增信息比较。只记录任务编号和统计;原话、权限检查和故事更新由故事平台负责。',nodes,edges)
|
||
|
|
(DEST/'chat-to-stories.yml').write_text(json.dumps(extract,ensure_ascii=False,indent=2),encoding='utf-8')
|
||
|
|
|
||
|
|
nodes=[start(),http('prepare','读取长者全部可用故事','prepare',360,80,kind='memoir',prepared=False),parse('prepared','接收整本任务 / 示例测试','prepare',{'context_id':'string','step_token':'string'},670,80),http('write-book','Doubao:全部故事统一编写整本','write-book',670,300,prompt=book_prompt),parse('written','检查完整章节与插画计划','write-book',{'chapter_count':'number','chapter_indexes':'array[number]'},360,300),http('validate-book','核验来源与日期 · 先显示正文','validate-book',50,300),parse('validated','所有故事均已成章','validate-book',{'chapter_count':'number'},50,520)]
|
||
|
|
next(n for n in nodes if n['id']=='write-book')['data']['timeout']['read']=660
|
||
|
|
iteration=node('pictures','iteration','逐章配图 · 并行 3 章',360,520,iterator_selector=['written','chapter_indexes'],iterator_input_type='array[number]',output_selector=['picture-result','result'],output_type='array[string]',is_parallel=True,parallel_nums=3,error_handle_mode='terminated',start_node_id='pictures-start',width=630,height=235)
|
||
|
|
iteration['width']=630;iteration['height']=235;iteration['zIndex']=1;nodes.append(iteration)
|
||
|
|
inner_start=node('pictures-start','iteration-start','',24,80,isInIteration=True);inner_start.update(type='custom-iteration-start',parentId='pictures',width=44,height=48,draggable=False,selectable=False,zIndex=1002)
|
||
|
|
image=http('illustrate','Seedream:绘制该章情景插画','illustrate',95,65,index=True);image.update(parentId='pictures',extent='parent',zIndex=1002);image['data'].update(isInIteration=True,isInLoop=False,iteration_id='pictures')
|
||
|
|
image_result=node('picture-result','code','保留正文,记录插画结果',365,65,code_language='python3',code='import json\ndef main(body: str) -> dict:\n data=json.loads(body)\n return {"result":json.dumps(data,ensure_ascii=False)}\n',variables=[{'variable':'body','value_selector':['illustrate','body']}],outputs={'result':{'type':'string','children':None}},isInIteration=True,isInLoop=False,iteration_id='pictures');image_result.update(parentId='pictures',extent='parent',zIndex=1002)
|
||
|
|
nodes += [inner_start,image,image_result,http('finish','汇总正文与插画完成情况','finish',50,850),parse('finished','结果核验与图片数量','finish',{'chapter_count':'number','illustrations_ready':'number','illustration_errors':'number'},360,850),end('finished',{'chapter_count':'number','illustrations_ready':'number','illustration_errors':'number'},670,850)]
|
||
|
|
main=['start','prepare','prepared','write-book','written','validate-book','validated','pictures','finish','finished','end'];edges=[edge(a,b,nodes) for a,b in zip(main,main[1:])]+[edge('pictures-start','illustrate',nodes,'pictures'),edge('illustrate','picture-result',nodes,'pictures')]
|
||
|
|
book=app('图文回忆录:全部故事成书与逐章配图','全部故事一次交给 Doubao-Seed-Evolving 统一成书 → 核验章节、事实与日期 → 正文先显示 → Seedream 并行配图 → 后台检查账号授权和来源版本并保存。',nodes,edges)
|
||
|
|
(DEST/'stories-to-memoir.yml').write_text(json.dumps(book,ensure_ascii=False,indent=2),encoding='utf-8')
|
||
|
|
if args.provider_id:
|
||
|
|
spec = importlib.util.spec_from_file_location('dify_model_tools', ROOT/'scripts/dify-model-tools.py')
|
||
|
|
tools = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(tools)
|
||
|
|
for workflow, filename in [(extract, 'chat-to-stories.yml'), (book, 'stories-to-memoir.yml')]:
|
||
|
|
workflow['workflow']['graph'] = tools.convert_graph(workflow['workflow']['graph'], args.provider_id)
|
||
|
|
(DEST/filename).write_text(json.dumps(workflow,ensure_ascii=False,indent=2),encoding='utf-8')
|
||
|
|
(DEST.parent/'model-tools.openapi.json').write_text(json.dumps(tools.schema(),ensure_ascii=False,indent=2),encoding='utf-8')
|
||
|
|
print(json.dumps({'workflows':2,'nodes':[len(extract['workflow']['graph']['nodes']),len(book['workflow']['graph']['nodes'])]}))
|