test_lab_routes.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. """网页测试工作台;与当前聊天接口使用相同的本地用户归属约定。"""
  2. import asyncio
  3. from typing import Any, Literal
  4. from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
  5. from fastapi.responses import FileResponse
  6. from pydantic import BaseModel, ConfigDict, Field, ValidationError
  7. import config
  8. from db import test_lab_store
  9. from runtime.business_tasks import get_controller
  10. from runtime.test_lab import files, service
  11. from runtime.test_lab.spreadsheets import preview_workbook
  12. from runtime.test_lab.main_fan_simulation import MainFanSimulation
  13. def enabled():
  14. if not config.get_bool('TEST_LAB_ENABLED',True):
  15. raise HTTPException(404,'测试工作台未启用')
  16. router=APIRouter(prefix='/test-lab',tags=['智能体测试'],dependencies=[Depends(enabled)])
  17. class TestRequest(BaseModel):
  18. model_config=ConfigDict(extra='forbid')
  19. agent_name:str=Field(min_length=1,max_length=64)
  20. scenario_id:str=Field(default='custom',max_length=64)
  21. label:str=Field(default='自定义测试',min_length=1,max_length=128)
  22. input:dict[str,Any]
  23. asset_ids:list[str]=Field(default_factory=list,max_length=20)
  24. input_asset_id:str|None=None
  25. execution_mode:Literal['pipeline','agent']='agent'
  26. request_id:str|None=Field(default=None,min_length=1,max_length=128)
  27. main_fan_simulation:MainFanSimulation|None=None
  28. def bad_request(exc):
  29. if isinstance(exc,ValidationError):
  30. return HTTPException(422,exc.errors(include_input=False,include_context=False))
  31. return HTTPException(400,str(exc))
  32. @router.get('/catalog')
  33. async def catalog(user_name:str=Query('default',min_length=1,max_length=128)):
  34. return await asyncio.to_thread(service.catalog,user_name)
  35. @router.get('/presets/{agent}/{identifier}')
  36. async def preset(agent:str,identifier:str):
  37. try:
  38. return await asyncio.to_thread(service.get_preset,agent,identifier)
  39. except ValueError as exc:
  40. raise HTTPException(404,str(exc)) from None
  41. @router.post('/assets',status_code=201)
  42. async def upload(file:UploadFile=File(...),user_name:str=Query('default',min_length=1,max_length=128)):
  43. try:
  44. return await files.upload(file,user_name)
  45. except (ValueError,UnicodeError) as exc:
  46. raise bad_request(exc) from None
  47. @router.get('/assets/{identifier}/content')
  48. async def asset_content(identifier:str,user_name:str='default',download:bool=False):
  49. try:
  50. path,description=await asyncio.to_thread(files.owned_file,identifier,user_name)
  51. except ValueError as exc:
  52. raise HTTPException(404,str(exc)) from None
  53. return FileResponse(path,media_type=files.media_type(path),filename=description['filename'],
  54. content_disposition_type='attachment' if download else 'inline',headers={'X-Content-Type-Options':'nosniff'})
  55. @router.get('/assets/{identifier}/preview')
  56. async def asset_preview(identifier:str,user_name:str='default'):
  57. try:
  58. path,description=await asyncio.to_thread(files.owned_file,identifier,user_name)
  59. if description['kind']=='json':
  60. return {'kind':'json','content':await asyncio.to_thread(files.json_value,path)}
  61. if description['kind']=='excel':
  62. return {'kind':'excel',**await asyncio.to_thread(preview_workbook,path)}
  63. return description
  64. except ValueError as exc:
  65. raise bad_request(exc) from None
  66. @router.post('/runs',status_code=202)
  67. async def start(payload:TestRequest,user_name:str=Query('default',min_length=1,max_length=128)):
  68. try:
  69. return await asyncio.to_thread(service.start_run,payload,user_name)
  70. except ValueError as exc:
  71. raise bad_request(exc) from None
  72. except RuntimeError as exc:
  73. raise HTTPException(503,str(exc)) from None
  74. @router.get('/runs')
  75. async def runs(user_name:str='default',limit:int=Query(50,ge=1,le=200)):
  76. rows=await asyncio.to_thread(test_lab_store.runs,user_name,limit)
  77. return {'items':[{k:v for k,v in r.items() if k!='request_json'} for r in rows]}
  78. @router.get('/runs/{identifier}')
  79. async def run(identifier:str,user_name:str='default'):
  80. try:
  81. return await asyncio.to_thread(service.public_run,identifier,user_name)
  82. except ValueError as exc:
  83. raise HTTPException(404,str(exc)) from None
  84. @router.post('/runs/{identifier}/cancel')
  85. async def cancel(identifier:str,user_name:str='default'):
  86. try:
  87. await asyncio.to_thread(test_lab_store.run,identifier,user_name)
  88. await get_controller().cancel_job(identifier)
  89. return await asyncio.to_thread(service.public_run,identifier,user_name)
  90. except ValueError as exc:
  91. raise HTTPException(404,str(exc)) from None
  92. @router.get('/runs/{identifier}/artifacts/{key}/content')
  93. async def artifact_content(identifier:str,key:str,user_name:str='default',download:bool=True):
  94. try:
  95. item=await asyncio.to_thread(service.artifact,identifier,key,user_name)
  96. except ValueError as exc:
  97. raise HTTPException(404,str(exc)) from None
  98. return FileResponse(item['path'],media_type=files.media_type(item['path']),filename=item['filename'],
  99. content_disposition_type='attachment' if download else 'inline',headers={'X-Content-Type-Options':'nosniff'})
  100. @router.get('/runs/{identifier}/artifacts/{key}/preview')
  101. async def artifact_preview(identifier:str,key:str,user_name:str='default'):
  102. try:
  103. item=await asyncio.to_thread(service.artifact,identifier,key,user_name)
  104. return {'kind':'excel','origin':item['origin'],**await asyncio.to_thread(preview_workbook,item['path'])}
  105. except ValueError as exc:
  106. raise bad_request(exc) from None