Files
zyt/deployment/followup-audio-dify/dify_operator.py
T

120 lines
8.8 KiB
Python

"""Execute in Dify API Python with PAYLOAD supplied by private host wrapper.
Uses deployed services/controllers for credential encryption and app config.
Never print this process output to public logs: create returns a new app key to
its private caller; the wrapper writes it 0600 and emits only public metadata.
"""
import hashlib
import inspect
import json
import logging
import sys
logging.disable(logging.CRITICAL)
from app_factory import create_app
app = create_app()
from sqlalchemy import select
from extensions.ext_database import db
from models.account import Account, Tenant, TenantAccountJoin
from models.model import App, ApiToken
from models.provider import ProviderModel, ProviderModelCredential, TenantDefaultModel
from services.app_service import AppService
from services.model_provider_service import ModelProviderService
PROVIDER = 'langgenius/openai_api_compatible/openai_api_compatible'
ASR = 'Qwen/Qwen3-ASR-1.7B'
APP_NAME = 'followup-audio-isolated-20261009'
ENDPOINT = 'http://followup-asr-bridge:8080/v1'
CONFIG = {
'model': {'provider': PROVIDER, 'name': 'qwen3.6-35b', 'mode': 'chat',
'completion_params': {'max_tokens': 8192, 'temperature': 0.0, 'top_p': 1.0, 'enable_thinking': False}},
'pre_prompt': '仅依据本次用户请求处理提供的文本。遵循用户给定的 JSON 格式,只返回最终 JSON,不输出思考过程、解释或 Markdown。不得把示例当成事实。',
'prompt_type': 'simple', 'user_input_form': [],
'speech_to_text': {'enabled': True}, 'text_to_speech': {'enabled': False},
'suggested_questions_after_answer': {'enabled': False},
'retriever_resource': {'enabled': False}, 'sensitive_word_avoidance': {'enabled': False},
'file_upload': {'enabled': False},
}
with app.app_context():
# Anchor only to the existing default Qwen workspace. Fail if ambiguous.
tenant_ids = db.session.scalars(select(TenantDefaultModel.tenant_id).where(
TenantDefaultModel.model_type == 'text-generation', TenantDefaultModel.model_name == 'qwen3.6-35b')).all()
assert len(tenant_ids) == 1, 'AMBIGUOUS_WORKSPACE'
tenant_id = tenant_ids[0]
tenant = db.session.get(Tenant, tenant_id)
owner = db.session.scalars(select(Account).join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id).where(
TenantAccountJoin.tenant_id == tenant_id, TenantAccountJoin.role == 'owner')).one()
owner.current_tenant = tenant
def current_app_owned():
resource = PAYLOAD['resources']
obj = db.session.get(App, resource['app_id'])
assert obj and obj.name == APP_NAME and obj.tenant_id == tenant_id, 'APP_IDENTITY_MISMATCH'
return obj
def public_state(obj):
model = db.session.scalars(select(ProviderModel).where(ProviderModel.tenant_id == tenant_id,
ProviderModel.provider_name == PROVIDER, ProviderModel.model_name == ASR, ProviderModel.model_type == 'speech2text')).one()
default = db.session.scalars(select(TenantDefaultModel).where(TenantDefaultModel.tenant_id == tenant_id,
TenantDefaultModel.model_type == 'speech2text')).one()
config = obj.app_model_config.to_dict()
return {'app_id':obj.id,'app_name':obj.name,'tenant_id':tenant_id,'app_mode':obj.mode,
'enable_api':obj.enable_api,'enable_site':obj.enable_site,'config_id':obj.app_model_config_id,
'speech_to_text':obj.app_model_config.speech_to_text_dict,
'model':obj.app_model_config.model_dict,'pre_prompt_sha256':hashlib.sha256(obj.app_model_config.pre_prompt.encode()).hexdigest(),
'config_sha256':hashlib.sha256(json.dumps(config,ensure_ascii=False,sort_keys=True,default=str).encode()).hexdigest(),
'asr_model_id':model.id,'asr_credential_id':model.credential_id,'asr_default_id':default.id,
'asr_model':model.model_name,'asr_default':default.model_name,'provider':PROVIDER,'endpoint':ENDPOINT,
'base_url':'https://ai.zhenyangtang.com.cn/v1','max_active_requests':obj.max_active_requests}
result = {}
with app.test_request_context('/', method='POST', json=CONFIG):
app.login_manager._update_request_context_with_user(owner)
svc = AppService()
action = PAYLOAD['action']
if action == 'create':
assert not db.session.scalar(select(App.id).where(App.name == APP_NAME)), 'APP_NAME_ALREADY_EXISTS'
assert not db.session.scalar(select(ProviderModel.id).where(ProviderModel.tenant_id == tenant_id,ProviderModel.model_type == 'speech2text')), 'SPEECH2TEXT_NOT_EMPTY'
assert not db.session.scalar(select(TenantDefaultModel.id).where(TenantDefaultModel.tenant_id == tenant_id,TenantDefaultModel.model_type == 'speech2text')), 'DEFAULT_NOT_EMPTY'
ModelProviderService().create_model_credential(tenant_id,PROVIDER,'speech2text',ASR,
{'api_key':PAYLOAD['asr_key'],'endpoint_url':ENDPOINT,'endpoint_model_name':ASR,'language':'zh','initial_prompt':'','display_name':'Followup ASR isolated'},
'followup-asr-isolated-20261009')
ModelProviderService().update_default_model_of_model_type(tenant_id,'speech2text',PROVIDER,ASR)
obj=svc.create_app(tenant_id,{'mode':'chat','name':APP_NAME,'description':'Isolated synthetic-verified ASR and JSON extraction. Preview only; no clinical writes.','icon_type':'emoji','icon':'🎙️','icon_background':'#E4FBCC'},owner)
svc.update_app_site_status(obj,False)
from controllers.console.app.model_config import ModelConfigResource
inspect.unwrap(ModelConfigResource.post)(ModelConfigResource(),obj)
svc.update_app(obj, {'name':APP_NAME,'description':obj.description,'icon_type':obj.icon_type,'icon':obj.icon,'icon_background':obj.icon_background,'use_icon_as_answer_icon':False,'max_active_requests':1})
svc.update_app_api_status(obj,True)
from controllers.console.apikey import AppApiKeyListResource, BaseApiKeyListResource
token,status=inspect.unwrap(BaseApiKeyListResource.post)(AppApiKeyListResource(),obj.id)
assert status == 201
result={'public':public_state(obj),'private':{'api_key':token.token,'key_id':token.id}}
elif action in ('status','disable','enable'):
obj=current_app_owned()
if action != 'status':svc.update_app_api_status(obj,action == 'enable')
result={'public':public_state(obj)}
elif action == 'remove':
resource=PAYLOAD['resources'];obj=current_app_owned()
credential=db.session.get(ProviderModelCredential,resource['asr_credential_id'])
recorded_default=db.session.get(TenantDefaultModel,resource['asr_default_id'])
assert credential and credential.tenant_id==tenant_id and credential.model_name==ASR and credential.model_type=='speech2text', 'CREDENTIAL_IDENTITY_MISMATCH'
assert recorded_default and recorded_default.tenant_id==tenant_id and recorded_default.model_name==ASR and recorded_default.model_type=='speech2text', 'DEFAULT_IDENTITY_MISMATCH'
for other in db.session.scalars(select(App).where(App.tenant_id==tenant_id,App.id!=obj.id)).all():
features=(other.workflow.features_dict if other.workflow else {}) if other.mode in ('advanced-chat','workflow') else ({'speech_to_text':other.app_model_config.speech_to_text_dict} if other.app_model_config else {})
assert not features.get('speech_to_text',{}).get('enabled'), 'ANOTHER_APP_NOW_USES_ASR'
svc.update_app_api_status(obj,False)
# Only the precise new app key, via the deployed API controller.
from controllers.console.apikey import AppApiKeyResource, BaseApiKeyResource
key_id=resource['key_id']
if db.session.get(ApiToken,key_id):
inspect.unwrap(BaseApiKeyResource.delete)(AppApiKeyResource(),obj.id,key_id)
svc.delete_app(obj)
default=db.session.get(TenantDefaultModel,resource['asr_default_id'])
assert default and default.tenant_id==tenant_id and default.model_type=='speech2text' and default.model_name==ASR, 'DEFAULT_IDENTITY_MISMATCH'
# No service offers reset-to-empty; remove only this recorded, new, non-secret default row.
db.session.delete(default);db.session.commit()
ModelProviderService().remove_model_credential(tenant_id,PROVIDER,'speech2text',ASR,resource['asr_credential_id'])
assert not db.session.scalar(select(App.id).where(App.id==resource['app_id']))
assert not db.session.scalar(select(ProviderModel.id).where(ProviderModel.id==resource['asr_model_id']))
result={'public':{'removed_app_id':resource['app_id'],'speech2text_restored_empty':True}}
else:raise ValueError('UNKNOWN_ACTION')
print('OPERATOR_RESULT='+json.dumps(result,ensure_ascii=False,default=str))