solace-agent-mesh 1.6.1__py3-none-any.whl → 1.13.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (481) hide show
  1. solace_agent_mesh/agent/adk/alembic/README +74 -0
  2. solace_agent_mesh/agent/adk/alembic/env.py +77 -0
  3. solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
  4. solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
  5. solace_agent_mesh/agent/adk/alembic.ini +112 -0
  6. solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
  7. solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +165 -1
  8. solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +163 -0
  9. solace_agent_mesh/agent/adk/callbacks.py +852 -109
  10. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +234 -36
  11. solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +52 -5
  12. solace_agent_mesh/agent/adk/mcp_content_processor.py +1 -1
  13. solace_agent_mesh/agent/adk/models/lite_llm.py +77 -21
  14. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +24 -137
  15. solace_agent_mesh/agent/adk/runner.py +85 -20
  16. solace_agent_mesh/agent/adk/schema_migration.py +88 -0
  17. solace_agent_mesh/agent/adk/services.py +94 -18
  18. solace_agent_mesh/agent/adk/setup.py +281 -65
  19. solace_agent_mesh/agent/adk/stream_parser.py +231 -37
  20. solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
  21. solace_agent_mesh/agent/protocol/event_handlers.py +472 -137
  22. solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
  23. solace_agent_mesh/agent/proxies/a2a/component.py +572 -75
  24. solace_agent_mesh/agent/proxies/a2a/config.py +80 -4
  25. solace_agent_mesh/agent/proxies/base/app.py +3 -2
  26. solace_agent_mesh/agent/proxies/base/component.py +188 -22
  27. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +3 -1
  28. solace_agent_mesh/agent/sac/app.py +91 -3
  29. solace_agent_mesh/agent/sac/component.py +591 -157
  30. solace_agent_mesh/agent/sac/patch_adk.py +8 -16
  31. solace_agent_mesh/agent/sac/task_execution_context.py +146 -4
  32. solace_agent_mesh/agent/tools/__init__.py +3 -0
  33. solace_agent_mesh/agent/tools/audio_tools.py +3 -3
  34. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +710 -171
  35. solace_agent_mesh/agent/tools/deep_research_tools.py +2161 -0
  36. solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
  37. solace_agent_mesh/agent/tools/peer_agent_tool.py +82 -15
  38. solace_agent_mesh/agent/tools/time_tools.py +126 -0
  39. solace_agent_mesh/agent/tools/tool_config_types.py +57 -2
  40. solace_agent_mesh/agent/tools/web_search_tools.py +279 -0
  41. solace_agent_mesh/agent/tools/web_tools.py +125 -17
  42. solace_agent_mesh/agent/utils/artifact_helpers.py +248 -6
  43. solace_agent_mesh/agent/utils/context_helpers.py +17 -0
  44. solace_agent_mesh/assets/docs/404.html +6 -6
  45. solace_agent_mesh/assets/docs/assets/css/{styles.906a1503.css → styles.8162edfb.css} +1 -1
  46. solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
  47. solace_agent_mesh/assets/docs/assets/js/15ba94aa.e186750d.js +1 -0
  48. solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
  49. solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
  50. solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
  51. solace_agent_mesh/assets/docs/assets/js/{17896441.a5e82f9b.js.LICENSE.txt → 2279.550aa580.js.LICENSE.txt} +6 -0
  52. solace_agent_mesh/assets/docs/assets/js/240a0364.83e37aa8.js +1 -0
  53. solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
  54. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.2f0db237.js +1 -0
  55. solace_agent_mesh/assets/docs/assets/js/3a6c6137.7e61915d.js +1 -0
  56. solace_agent_mesh/assets/docs/assets/js/3ac1795d.7f7ab1c1.js +1 -0
  57. solace_agent_mesh/assets/docs/assets/js/3ff0015d.e53c9b78.js +1 -0
  58. solace_agent_mesh/assets/docs/assets/js/41adc471.0e95b87c.js +1 -0
  59. solace_agent_mesh/assets/docs/assets/js/4667dc50.bf2ad456.js +1 -0
  60. solace_agent_mesh/assets/docs/assets/js/49eed117.493d6f99.js +1 -0
  61. solace_agent_mesh/assets/docs/assets/js/{509e993c.4c7a1a6d.js → 509e993c.a1fbf45a.js} +1 -1
  62. solace_agent_mesh/assets/docs/assets/js/547e15cc.8e6da617.js +1 -0
  63. solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
  64. solace_agent_mesh/assets/docs/assets/js/5b8d9c11.d4eb37b8.js +1 -0
  65. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.1ee87753.js +1 -0
  66. solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
  67. solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
  68. solace_agent_mesh/assets/docs/assets/js/64195356.09dbd087.js +1 -0
  69. solace_agent_mesh/assets/docs/assets/js/66d4869e.30340bd3.js +1 -0
  70. solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
  71. solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
  72. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
  73. solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
  74. solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
  75. solace_agent_mesh/assets/docs/assets/js/729898df.7249e9fd.js +1 -0
  76. solace_agent_mesh/assets/docs/assets/js/7e294c01.7c5f6906.js +1 -0
  77. solace_agent_mesh/assets/docs/assets/js/8024126c.e3467286.js +1 -0
  78. solace_agent_mesh/assets/docs/assets/js/81a99df0.7ed65d45.js +1 -0
  79. solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
  80. solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
  81. solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
  82. solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
  83. solace_agent_mesh/assets/docs/assets/js/9bb13469.4523ae20.js +1 -0
  84. solace_agent_mesh/assets/docs/assets/js/a7d42657.a956689d.js +1 -0
  85. solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
  86. solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e563275.js +1 -0
  87. solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
  88. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
  89. solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
  90. solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
  91. solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
  92. solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
  93. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
  94. solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
  95. solace_agent_mesh/assets/docs/assets/js/e04b235d.06d23db6.js +1 -0
  96. solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.deb2b62e.js +1 -0
  97. solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
  98. solace_agent_mesh/assets/docs/assets/js/e6f9706b.acc800d3.js +1 -0
  99. solace_agent_mesh/assets/docs/assets/js/e92d0134.c147a429.js +1 -0
  100. solace_agent_mesh/assets/docs/assets/js/ee0c2fe7.94d0a351.js +1 -0
  101. solace_agent_mesh/assets/docs/assets/js/f284c35a.cc97854c.js +1 -0
  102. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
  103. solace_agent_mesh/assets/docs/assets/js/main.d634009f.js +2 -0
  104. solace_agent_mesh/assets/docs/assets/js/runtime~main.27bb82a7.js +1 -0
  105. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +68 -68
  106. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +50 -50
  107. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +42 -42
  108. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +55 -55
  109. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +82 -68
  110. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/image-tools/index.html +81 -0
  111. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +67 -50
  112. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/research-tools/index.html +136 -0
  113. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +178 -144
  114. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +43 -42
  115. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +20 -18
  116. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +23 -23
  117. solace_agent_mesh/assets/docs/docs/documentation/components/platform-service/index.html +33 -0
  118. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +45 -45
  119. solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
  120. solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
  121. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +208 -125
  122. solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
  123. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +28 -49
  124. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +29 -30
  125. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +14 -14
  126. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/index.html +47 -0
  127. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/kubernetes-deployment-guide/index.html +197 -0
  128. solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +90 -0
  129. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +17 -16
  130. solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
  131. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +38 -38
  132. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +162 -171
  133. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +67 -49
  134. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +17 -17
  135. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +51 -51
  136. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +22 -22
  137. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +27 -27
  138. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +135 -135
  139. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +66 -66
  140. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +51 -51
  141. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +50 -38
  142. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +86 -86
  143. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +51 -51
  144. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +24 -24
  145. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +30 -30
  146. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +44 -44
  147. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
  148. solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
  149. solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
  150. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +23 -19
  151. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +40 -37
  152. solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
  153. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +112 -87
  154. solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
  155. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +87 -64
  156. solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
  157. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +44 -44
  158. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +39 -37
  159. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +30 -30
  160. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +18 -18
  161. solace_agent_mesh/assets/docs/docs/documentation/getting-started/vibe_coding/index.html +62 -0
  162. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +311 -0
  163. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +39 -42
  164. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +14 -14
  165. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +27 -25
  166. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +69 -69
  167. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +72 -72
  168. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
  169. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
  170. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +42 -42
  171. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +20 -20
  172. solace_agent_mesh/assets/docs/docs/documentation/migrations/platform-service-split/index.html +85 -0
  173. solace_agent_mesh/assets/docs/lunr-index-1768329217460.json +1 -0
  174. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  175. solace_agent_mesh/assets/docs/search-doc-1768329217460.json +1 -0
  176. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  177. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  178. solace_agent_mesh/cli/__init__.py +1 -1
  179. solace_agent_mesh/cli/commands/add_cmd/__init__.py +3 -1
  180. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -1
  181. solace_agent_mesh/cli/commands/add_cmd/proxy_cmd.py +100 -0
  182. solace_agent_mesh/cli/commands/docs_cmd.py +4 -1
  183. solace_agent_mesh/cli/commands/eval_cmd.py +1 -1
  184. solace_agent_mesh/cli/commands/init_cmd/__init__.py +15 -0
  185. solace_agent_mesh/cli/commands/init_cmd/directory_step.py +1 -1
  186. solace_agent_mesh/cli/commands/init_cmd/env_step.py +30 -3
  187. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +3 -4
  188. solace_agent_mesh/cli/commands/init_cmd/platform_service_step.py +85 -0
  189. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +16 -3
  190. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +2 -1
  191. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
  192. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +3 -3
  193. solace_agent_mesh/cli/commands/run_cmd.py +64 -49
  194. solace_agent_mesh/cli/commands/tools_cmd.py +315 -0
  195. solace_agent_mesh/cli/main.py +15 -0
  196. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-KnKMP_vb.js} +1 -1
  197. solace_agent_mesh/client/webui/frontend/static/assets/client-DpBL2stg.js +25 -0
  198. solace_agent_mesh/client/webui/frontend/static/assets/main-Cd498TV2.js +435 -0
  199. solace_agent_mesh/client/webui/frontend/static/assets/main-rSf8Vu29.css +1 -0
  200. solace_agent_mesh/client/webui/frontend/static/assets/vendor-CGk8Suyh.js +565 -0
  201. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  202. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  203. solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
  204. solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
  205. solace_agent_mesh/common/a2a/events.py +2 -1
  206. solace_agent_mesh/common/a2a/protocol.py +5 -0
  207. solace_agent_mesh/common/a2a/types.py +2 -1
  208. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +23 -6
  209. solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
  210. solace_agent_mesh/common/agent_registry.py +38 -11
  211. solace_agent_mesh/common/data_parts.py +144 -4
  212. solace_agent_mesh/common/error_handlers.py +83 -0
  213. solace_agent_mesh/common/exceptions.py +24 -0
  214. solace_agent_mesh/common/oauth/__init__.py +17 -0
  215. solace_agent_mesh/common/oauth/oauth_client.py +408 -0
  216. solace_agent_mesh/common/oauth/utils.py +50 -0
  217. solace_agent_mesh/common/rag_dto.py +156 -0
  218. solace_agent_mesh/common/sac/sam_component_base.py +97 -19
  219. solace_agent_mesh/common/sam_events/event_service.py +2 -2
  220. solace_agent_mesh/common/services/employee_service.py +1 -1
  221. solace_agent_mesh/common/utils/embeds/constants.py +1 -0
  222. solace_agent_mesh/common/utils/embeds/converter.py +1 -8
  223. solace_agent_mesh/common/utils/embeds/modifiers.py +4 -28
  224. solace_agent_mesh/common/utils/embeds/resolver.py +152 -31
  225. solace_agent_mesh/common/utils/embeds/types.py +9 -0
  226. solace_agent_mesh/common/utils/log_formatters.py +20 -0
  227. solace_agent_mesh/common/utils/mime_helpers.py +12 -5
  228. solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
  229. solace_agent_mesh/common/utils/rbac_utils.py +69 -0
  230. solace_agent_mesh/common/utils/templates/__init__.py +8 -0
  231. solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
  232. solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
  233. solace_agent_mesh/config_portal/backend/common.py +12 -0
  234. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-CljP4_mv.js +103 -0
  235. solace_agent_mesh/config_portal/frontend/static/client/assets/{components-Rk0n-9cK.js → components-CaC6hG8d.js} +22 -22
  236. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-mvZjNKiz.js → entry.client-H_TM0YBt.js} +3 -3
  237. solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DzNKzXrc.js → index-CnFykb2v.js} +16 -16
  238. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-f8439d40.js +1 -0
  239. solace_agent_mesh/config_portal/frontend/static/client/assets/root-BIMqslJB.css +1 -0
  240. solace_agent_mesh/config_portal/frontend/static/client/assets/root-mJmTIdIk.js +10 -0
  241. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  242. solace_agent_mesh/core_a2a/service.py +3 -2
  243. solace_agent_mesh/gateway/adapter/__init__.py +1 -0
  244. solace_agent_mesh/gateway/adapter/base.py +170 -0
  245. solace_agent_mesh/gateway/adapter/types.py +230 -0
  246. solace_agent_mesh/gateway/base/app.py +39 -2
  247. solace_agent_mesh/gateway/base/auth_interface.py +103 -0
  248. solace_agent_mesh/gateway/base/component.py +1027 -151
  249. solace_agent_mesh/gateway/generic/__init__.py +1 -0
  250. solace_agent_mesh/gateway/generic/app.py +50 -0
  251. solace_agent_mesh/gateway/generic/component.py +894 -0
  252. solace_agent_mesh/gateway/http_sse/alembic/env.py +0 -7
  253. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
  254. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
  255. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
  256. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
  257. solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
  258. solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
  259. solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
  260. solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
  261. solace_agent_mesh/gateway/http_sse/alembic.ini +0 -36
  262. solace_agent_mesh/gateway/http_sse/app.py +40 -11
  263. solace_agent_mesh/gateway/http_sse/component.py +285 -160
  264. solace_agent_mesh/gateway/http_sse/dependencies.py +149 -114
  265. solace_agent_mesh/gateway/http_sse/main.py +68 -450
  266. solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
  267. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +2 -2
  268. solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
  269. solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
  270. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +26 -3
  271. solace_agent_mesh/gateway/http_sse/repository/entities/task.py +7 -0
  272. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
  273. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +114 -6
  274. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +13 -0
  275. solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
  276. solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
  277. solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
  278. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +8 -2
  279. solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +8 -1
  280. solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
  281. solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
  282. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +177 -11
  283. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +86 -2
  284. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +38 -7
  285. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +256 -58
  286. solace_agent_mesh/gateway/http_sse/routers/auth.py +168 -134
  287. solace_agent_mesh/gateway/http_sse/routers/config.py +302 -8
  288. solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
  289. solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
  290. solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
  291. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +14 -1
  292. solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +1 -1
  293. solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
  294. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +5 -2
  295. solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
  296. solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
  297. solace_agent_mesh/gateway/http_sse/routers/people.py +2 -2
  298. solace_agent_mesh/gateway/http_sse/routers/projects.py +768 -0
  299. solace_agent_mesh/gateway/http_sse/routers/prompts.py +1416 -0
  300. solace_agent_mesh/gateway/http_sse/routers/sessions.py +167 -7
  301. solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
  302. solace_agent_mesh/gateway/http_sse/routers/sse.py +131 -8
  303. solace_agent_mesh/gateway/http_sse/routers/tasks.py +670 -18
  304. solace_agent_mesh/gateway/http_sse/routers/users.py +1 -1
  305. solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
  306. solace_agent_mesh/gateway/http_sse/routers/visualization.py +92 -9
  307. solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
  308. solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
  309. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +1 -1
  310. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +1 -1
  311. solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
  312. solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
  313. solace_agent_mesh/gateway/http_sse/services/session_service.py +361 -12
  314. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +354 -4
  315. solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
  316. solace_agent_mesh/gateway/http_sse/sse_manager.py +286 -166
  317. solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
  318. solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +41 -1
  319. solace_agent_mesh/services/__init__.py +0 -0
  320. solace_agent_mesh/services/platform/__init__.py +29 -0
  321. solace_agent_mesh/services/platform/alembic/env.py +85 -0
  322. solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
  323. solace_agent_mesh/services/platform/alembic.ini +109 -0
  324. solace_agent_mesh/services/platform/api/__init__.py +3 -0
  325. solace_agent_mesh/services/platform/api/dependencies.py +154 -0
  326. solace_agent_mesh/services/platform/api/main.py +314 -0
  327. solace_agent_mesh/services/platform/api/middleware.py +51 -0
  328. solace_agent_mesh/services/platform/api/routers/__init__.py +33 -0
  329. solace_agent_mesh/services/platform/api/routers/health_router.py +31 -0
  330. solace_agent_mesh/services/platform/app.py +215 -0
  331. solace_agent_mesh/services/platform/component.py +777 -0
  332. solace_agent_mesh/shared/__init__.py +14 -0
  333. solace_agent_mesh/shared/api/__init__.py +42 -0
  334. solace_agent_mesh/shared/auth/__init__.py +26 -0
  335. solace_agent_mesh/shared/auth/dependencies.py +204 -0
  336. solace_agent_mesh/shared/auth/middleware.py +347 -0
  337. solace_agent_mesh/shared/database/__init__.py +20 -0
  338. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/base_repository.py +1 -1
  339. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_exceptions.py +1 -1
  340. solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_helpers.py +1 -1
  341. solace_agent_mesh/shared/exceptions/__init__.py +36 -0
  342. solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exception_handlers.py +19 -5
  343. solace_agent_mesh/shared/utils/__init__.py +21 -0
  344. solace_agent_mesh/templates/logging_config_template.yaml +48 -0
  345. solace_agent_mesh/templates/main_orchestrator.yaml +12 -1
  346. solace_agent_mesh/templates/platform.yaml +49 -0
  347. solace_agent_mesh/templates/plugin_readme_template.md +3 -25
  348. solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
  349. solace_agent_mesh/templates/proxy_template.yaml +62 -0
  350. solace_agent_mesh/templates/webui.yaml +148 -6
  351. solace_agent_mesh/tools/web_search/__init__.py +18 -0
  352. solace_agent_mesh/tools/web_search/base.py +84 -0
  353. solace_agent_mesh/tools/web_search/google_search.py +247 -0
  354. solace_agent_mesh/tools/web_search/models.py +99 -0
  355. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/METADATA +31 -12
  356. solace_agent_mesh-1.13.2.dist-info/RECORD +591 -0
  357. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/WHEEL +1 -1
  358. solace_agent_mesh/agent/adk/adk_llm.txt +0 -232
  359. solace_agent_mesh/agent/adk/adk_llm_detail.txt +0 -566
  360. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +0 -171
  361. solace_agent_mesh/agent/adk/models/models_llm.txt +0 -142
  362. solace_agent_mesh/agent/agent_llm.txt +0 -378
  363. solace_agent_mesh/agent/agent_llm_detail.txt +0 -1702
  364. solace_agent_mesh/agent/protocol/protocol_llm.txt +0 -81
  365. solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +0 -92
  366. solace_agent_mesh/agent/sac/sac_llm.txt +0 -189
  367. solace_agent_mesh/agent/sac/sac_llm_detail.txt +0 -200
  368. solace_agent_mesh/agent/testing/testing_llm.txt +0 -57
  369. solace_agent_mesh/agent/testing/testing_llm_detail.txt +0 -68
  370. solace_agent_mesh/agent/tools/tools_llm.txt +0 -263
  371. solace_agent_mesh/agent/tools/tools_llm_detail.txt +0 -274
  372. solace_agent_mesh/agent/utils/utils_llm.txt +0 -138
  373. solace_agent_mesh/agent/utils/utils_llm_detail.txt +0 -149
  374. solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.js +0 -1
  375. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +0 -2
  376. solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
  377. solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +0 -1
  378. solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +0 -1
  379. solace_agent_mesh/assets/docs/assets/js/3ac1795d.76654dd9.js +0 -1
  380. solace_agent_mesh/assets/docs/assets/js/3ff0015d.2be20244.js +0 -1
  381. solace_agent_mesh/assets/docs/assets/js/547e15cc.2cbb060a.js +0 -1
  382. solace_agent_mesh/assets/docs/assets/js/55b7b518.f2b1d1ba.js +0 -1
  383. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +0 -1
  384. solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
  385. solace_agent_mesh/assets/docs/assets/js/6a520c9d.ba015d81.js +0 -1
  386. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +0 -1
  387. solace_agent_mesh/assets/docs/assets/js/6d84eae0.4a5fbf39.js +0 -1
  388. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
  389. solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
  390. solace_agent_mesh/assets/docs/assets/js/81a99df0.07034dd9.js +0 -1
  391. solace_agent_mesh/assets/docs/assets/js/82fbfb93.139a1a1f.js +0 -1
  392. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +0 -1
  393. solace_agent_mesh/assets/docs/assets/js/94e8668d.b5ddb7a1.js +0 -1
  394. solace_agent_mesh/assets/docs/assets/js/9bb13469.dd1c9b54.js +0 -1
  395. solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +0 -1
  396. solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e6dd091.js +0 -1
  397. solace_agent_mesh/assets/docs/assets/js/c93cbaa0.eaff365e.js +0 -1
  398. solace_agent_mesh/assets/docs/assets/js/da0b5bad.d08a9466.js +0 -1
  399. solace_agent_mesh/assets/docs/assets/js/dd817ffc.0aa9630a.js +0 -1
  400. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.d590bc9e.js +0 -1
  401. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +0 -1
  402. solace_agent_mesh/assets/docs/assets/js/e3d9abda.6b9493d0.js +0 -1
  403. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +0 -1
  404. solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
  405. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
  406. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
  407. solace_agent_mesh/assets/docs/assets/js/main.b12eac43.js +0 -2
  408. solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +0 -1
  409. solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +0 -1
  410. solace_agent_mesh/assets/docs/search-doc-1761248203150.json +0 -1
  411. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +0 -250
  412. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +0 -365
  413. solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +0 -305
  414. solace_agent_mesh/client/webui/frontend/static/assets/client-CaY59VuC.js +0 -25
  415. solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +0 -342
  416. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
  417. solace_agent_mesh/client/webui/frontend/static/assets/vendor-BEmvJSYz.js +0 -405
  418. solace_agent_mesh/common/a2a/a2a_llm.txt +0 -182
  419. solace_agent_mesh/common/a2a/a2a_llm_detail.txt +0 -193
  420. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +0 -407
  421. solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +0 -736
  422. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +0 -313
  423. solace_agent_mesh/common/common_llm.txt +0 -251
  424. solace_agent_mesh/common/common_llm_detail.txt +0 -2562
  425. solace_agent_mesh/common/middleware/middleware_llm.txt +0 -174
  426. solace_agent_mesh/common/middleware/middleware_llm_detail.txt +0 -185
  427. solace_agent_mesh/common/sac/sac_llm.txt +0 -71
  428. solace_agent_mesh/common/sac/sac_llm_detail.txt +0 -82
  429. solace_agent_mesh/common/sam_events/sam_events_llm.txt +0 -104
  430. solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +0 -115
  431. solace_agent_mesh/common/services/providers/providers_llm.txt +0 -80
  432. solace_agent_mesh/common/services/services_llm.txt +0 -363
  433. solace_agent_mesh/common/services/services_llm_detail.txt +0 -459
  434. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +0 -220
  435. solace_agent_mesh/common/utils/utils_llm.txt +0 -336
  436. solace_agent_mesh/common/utils/utils_llm_detail.txt +0 -572
  437. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +0 -98
  438. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-61038fc6.js +0 -1
  439. solace_agent_mesh/config_portal/frontend/static/client/assets/root-BWvk5-gF.js +0 -10
  440. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +0 -1
  441. solace_agent_mesh/core_a2a/core_a2a_llm.txt +0 -90
  442. solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +0 -101
  443. solace_agent_mesh/gateway/base/base_llm.txt +0 -224
  444. solace_agent_mesh/gateway/base/base_llm_detail.txt +0 -235
  445. solace_agent_mesh/gateway/gateway_llm.txt +0 -373
  446. solace_agent_mesh/gateway/gateway_llm_detail.txt +0 -3885
  447. solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +0 -295
  448. solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +0 -155
  449. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +0 -105
  450. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +0 -299
  451. solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +0 -3278
  452. solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +0 -263
  453. solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +0 -266
  454. solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +0 -340
  455. solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +0 -346
  456. solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +0 -83
  457. solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +0 -107
  458. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +0 -314
  459. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +0 -297
  460. solace_agent_mesh/gateway/http_sse/shared/__init__.py +0 -146
  461. solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +0 -285
  462. solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +0 -47
  463. solace_agent_mesh/llm.txt +0 -228
  464. solace_agent_mesh/llm_detail.txt +0 -2835
  465. solace_agent_mesh/solace_agent_mesh_llm.txt +0 -362
  466. solace_agent_mesh/solace_agent_mesh_llm_detail.txt +0 -8599
  467. solace_agent_mesh/templates/logging_config_template.ini +0 -45
  468. solace_agent_mesh/templates/templates_llm.txt +0 -147
  469. solace_agent_mesh-1.6.1.dist-info/RECORD +0 -525
  470. /solace_agent_mesh/assets/docs/assets/js/{main.b12eac43.js.LICENSE.txt → main.d634009f.js.LICENSE.txt} +0 -0
  471. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/auth_utils.py +0 -0
  472. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/pagination.py +0 -0
  473. /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/response_utils.py +0 -0
  474. /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/error_dto.py +0 -0
  475. /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exceptions.py +0 -0
  476. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/enums.py +0 -0
  477. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/timestamp_utils.py +0 -0
  478. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/types.py +0 -0
  479. /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/utils.py +0 -0
  480. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/entry_points.txt +0 -0
  481. {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,930 @@
1
+ """
2
+ Business service for project-related operations.
3
+ """
4
+
5
+ from typing import List, Optional, TYPE_CHECKING
6
+ import logging
7
+ import json
8
+ import zipfile
9
+ from io import BytesIO
10
+ from fastapi import UploadFile
11
+ from datetime import datetime, timezone
12
+
13
+ from ....agent.utils.artifact_helpers import get_artifact_info_list, save_artifact_with_metadata, get_artifact_counts_batch
14
+
15
+ # Default max upload size (50MB) - matches gateway_max_upload_size_bytes default
16
+ DEFAULT_MAX_UPLOAD_SIZE_BYTES = 52428800
17
+ # Default max ZIP upload size (100MB) - for project import ZIP files
18
+ DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES = 104857600
19
+
20
+ try:
21
+ from google.adk.artifacts import BaseArtifactService
22
+ except ImportError:
23
+
24
+ class BaseArtifactService:
25
+ pass
26
+
27
+
28
+ from ....common.a2a.types import ArtifactInfo
29
+ from ..repository.interfaces import IProjectRepository
30
+ from ..repository.entities.project import Project
31
+
32
+ if TYPE_CHECKING:
33
+ from ..component import WebUIBackendComponent
34
+
35
+
36
+ class ProjectService:
37
+ """Service layer for project business logic."""
38
+
39
+ def __init__(
40
+ self,
41
+ component: "WebUIBackendComponent" = None,
42
+ ):
43
+ self.component = component
44
+ self.artifact_service = component.get_shared_artifact_service() if component else None
45
+ self.app_name = component.get_config("name", "WebUIBackendApp") if component else "WebUIBackendApp"
46
+ self.logger = logging.getLogger(__name__)
47
+ # Get max upload size from component config, with fallback to default
48
+ # Ensure values are integers for proper formatting
49
+ max_upload_config = (
50
+ component.get_config("gateway_max_upload_size_bytes", DEFAULT_MAX_UPLOAD_SIZE_BYTES)
51
+ if component else DEFAULT_MAX_UPLOAD_SIZE_BYTES
52
+ )
53
+ self.max_upload_size_bytes = int(max_upload_config) if isinstance(max_upload_config, (int, float)) else DEFAULT_MAX_UPLOAD_SIZE_BYTES
54
+
55
+ # Get max ZIP upload size from component config, with fallback to default (100MB)
56
+ max_zip_config = (
57
+ component.get_config("gateway_max_zip_upload_size_bytes", DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES)
58
+ if component else DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES
59
+ )
60
+ self.max_zip_upload_size_bytes = int(max_zip_config) if isinstance(max_zip_config, (int, float)) else DEFAULT_MAX_ZIP_UPLOAD_SIZE_BYTES
61
+
62
+ self.logger.info(
63
+ "[ProjectService] Initialized with max_upload_size_bytes=%d (%.2f MB), "
64
+ "max_zip_upload_size_bytes=%d (%.2f MB)",
65
+ self.max_upload_size_bytes,
66
+ self.max_upload_size_bytes / (1024*1024),
67
+ self.max_zip_upload_size_bytes,
68
+ self.max_zip_upload_size_bytes / (1024*1024)
69
+ )
70
+
71
+ def _get_repositories(self, db):
72
+ """Create project repository for the given database session."""
73
+ from ..repository.project_repository import ProjectRepository
74
+ return ProjectRepository(db)
75
+
76
+ def is_persistence_enabled(self) -> bool:
77
+ """Checks if the service is configured with a persistent backend."""
78
+ return self.component and self.component.database_url is not None
79
+
80
+ async def _validate_file_size(self, file: UploadFile, log_prefix: str = "") -> bytes:
81
+ """
82
+ Validate file size and read content with size checking.
83
+
84
+ Args:
85
+ file: The uploaded file to validate
86
+ log_prefix: Prefix for log messages
87
+
88
+ Returns:
89
+ bytes: The file content if validation passes
90
+
91
+ Raises:
92
+ ValueError: If file exceeds maximum allowed size
93
+ """
94
+ # Read file content in chunks to validate size
95
+ chunk_size = 1024 * 1024 # 1MB chunks
96
+ content_bytes = bytearray()
97
+ total_bytes_read = 0
98
+
99
+ while True:
100
+ chunk = await file.read(chunk_size)
101
+ if not chunk:
102
+ break
103
+
104
+ chunk_len = len(chunk)
105
+ total_bytes_read += chunk_len
106
+
107
+ # Validate size during reading (fail fast)
108
+ if total_bytes_read > self.max_upload_size_bytes:
109
+ error_msg = (
110
+ f"File '{file.filename}' rejected: size exceeds maximum "
111
+ f"{self.max_upload_size_bytes:,} bytes "
112
+ f"({self.max_upload_size_bytes / (1024*1024):.2f} MB). "
113
+ f"Read {total_bytes_read:,} bytes so far."
114
+ )
115
+ self.logger.warning(f"{log_prefix} {error_msg}")
116
+ raise ValueError(error_msg)
117
+
118
+ content_bytes.extend(chunk)
119
+
120
+ return bytes(content_bytes)
121
+
122
+ async def _validate_files(
123
+ self,
124
+ files: List[UploadFile],
125
+ log_prefix: str = ""
126
+ ) -> List[tuple]:
127
+ """
128
+ Validate multiple files and return their content.
129
+
130
+ Args:
131
+ files: List of uploaded files to validate
132
+ log_prefix: Prefix for log messages
133
+
134
+ Returns:
135
+ List of tuples: [(file, content_bytes), ...]
136
+
137
+ Raises:
138
+ ValueError: If any file exceeds maximum allowed size
139
+ """
140
+ validated_files = []
141
+ for file in files:
142
+ content_bytes = await self._validate_file_size(file, log_prefix)
143
+ validated_files.append((file, content_bytes))
144
+ self.logger.debug(
145
+ f"{log_prefix} Validated file '{file.filename}': {len(content_bytes):,} bytes"
146
+ )
147
+ return validated_files
148
+
149
+ async def create_project(
150
+ self,
151
+ db,
152
+ name: str,
153
+ user_id: str,
154
+ description: Optional[str] = None,
155
+ system_prompt: Optional[str] = None,
156
+ default_agent_id: Optional[str] = None,
157
+ files: Optional[List[UploadFile]] = None,
158
+ file_metadata: Optional[dict] = None,
159
+ ) -> Project:
160
+ """
161
+ Create a new project for a user.
162
+
163
+ Args:
164
+ db: Database session
165
+ name: Project name
166
+ user_id: ID of the user creating the project
167
+ description: Optional project description
168
+ system_prompt: Optional system prompt
169
+ default_agent_id: Optional default agent ID for new chats
170
+ files: Optional list of files to associate with the project
171
+
172
+ Returns:
173
+ DomainProject: The created project
174
+
175
+ Raises:
176
+ ValueError: If project name is invalid, user_id is missing, or file size exceeds limit
177
+ """
178
+ log_prefix = f"[ProjectService:create_project] User {user_id}:"
179
+ self.logger.info(f"Creating new project '{name}' for user {user_id}")
180
+
181
+ # Business validation
182
+ if not name or not name.strip():
183
+ raise ValueError("Project name cannot be empty")
184
+
185
+ if not user_id:
186
+ raise ValueError("User ID is required to create a project")
187
+
188
+ # Validate file sizes before creating project
189
+ validated_files = []
190
+ if files:
191
+ self.logger.info(f"{log_prefix} Validating {len(files)} files before project creation")
192
+ validated_files = await self._validate_files(files, log_prefix)
193
+ self.logger.info(f"{log_prefix} All {len(files)} files passed size validation")
194
+
195
+ project_repository = self._get_repositories(db)
196
+
197
+ # Check for duplicate project name for this user
198
+ existing_projects = project_repository.get_user_projects(user_id)
199
+ if any(p.name.lower() == name.strip().lower() for p in existing_projects):
200
+ raise ValueError(f"A project with the name '{name.strip()}' already exists")
201
+
202
+ # Create the project
203
+ project_domain = project_repository.create_project(
204
+ name=name.strip(),
205
+ user_id=user_id,
206
+ description=description.strip() if description else None,
207
+ system_prompt=system_prompt.strip() if system_prompt else None,
208
+ default_agent_id=default_agent_id,
209
+ )
210
+
211
+ if validated_files and self.artifact_service:
212
+ self.logger.info(
213
+ f"Project {project_domain.id} created, now saving {len(validated_files)} artifacts."
214
+ )
215
+ project_session_id = f"project-{project_domain.id}"
216
+ for file, content_bytes in validated_files:
217
+ metadata = {"source": "project"}
218
+ if file_metadata and file.filename in file_metadata:
219
+ desc = file_metadata[file.filename]
220
+ if desc:
221
+ metadata["description"] = desc
222
+
223
+ await save_artifact_with_metadata(
224
+ artifact_service=self.artifact_service,
225
+ app_name=self.app_name,
226
+ user_id=project_domain.user_id,
227
+ session_id=project_session_id,
228
+ filename=file.filename,
229
+ content_bytes=content_bytes,
230
+ mime_type=file.content_type,
231
+ metadata_dict=metadata,
232
+ timestamp=datetime.now(timezone.utc),
233
+ )
234
+ self.logger.info(f"Saved {len(validated_files)} artifacts for project {project_domain.id}")
235
+
236
+ self.logger.info(
237
+ f"Successfully created project {project_domain.id} for user {user_id}"
238
+ )
239
+ return project_domain
240
+
241
+ def get_project(self, db, project_id: str, user_id: str) -> Optional[Project]:
242
+ """
243
+ Get a project by ID, ensuring the user has access to it.
244
+
245
+ Args:
246
+ db: Database session
247
+ project_id: The project ID
248
+ user_id: The requesting user ID
249
+
250
+ Returns:
251
+ Optional[Project]: The project if found and accessible, None otherwise
252
+ """
253
+ project_repository = self._get_repositories(db)
254
+ return project_repository.get_by_id(project_id, user_id)
255
+
256
+ def get_user_projects(self, db, user_id: str) -> List[Project]:
257
+ """
258
+ Get all projects owned by a specific user.
259
+
260
+ Args:
261
+ db: Database session
262
+ user_id: The user ID
263
+
264
+ Returns:
265
+ List[DomainProject]: List of user's projects
266
+ """
267
+ self.logger.debug(f"Retrieving projects for user {user_id}")
268
+ project_repository = self._get_repositories(db)
269
+ db_projects = project_repository.get_user_projects(user_id)
270
+ return db_projects
271
+
272
+ async def get_user_projects_with_counts(self, db, user_id: str) -> List[tuple[Project, int]]:
273
+ """
274
+ Get all projects owned by a specific user with artifact counts.
275
+ Uses batch counting for efficiency.
276
+
277
+ Args:
278
+ db: Database session
279
+ user_id: The user ID
280
+
281
+ Returns:
282
+ List[tuple[Project, int]]: List of tuples (project, artifact_count)
283
+ """
284
+ self.logger.debug(f"Retrieving projects with artifact counts for user {user_id}")
285
+ projects = self.get_user_projects(db, user_id)
286
+
287
+ if not self.artifact_service or not projects:
288
+ # If no artifact service or no projects, return projects with 0 counts
289
+ return [(project, 0) for project in projects]
290
+
291
+ # Build list of session IDs for batch counting
292
+ session_ids = [f"project-{project.id}" for project in projects]
293
+
294
+ try:
295
+ # Get all counts in a single batch operation
296
+ counts_by_session = await get_artifact_counts_batch(
297
+ artifact_service=self.artifact_service,
298
+ app_name=self.app_name,
299
+ user_id=user_id,
300
+ session_ids=session_ids,
301
+ )
302
+
303
+ # Map counts back to projects
304
+ projects_with_counts = []
305
+ for project in projects:
306
+ storage_session_id = f"project-{project.id}"
307
+ artifact_count = counts_by_session.get(storage_session_id, 0)
308
+ projects_with_counts.append((project, artifact_count))
309
+
310
+ self.logger.debug(f"Retrieved artifact counts for {len(projects)} projects in batch")
311
+ return projects_with_counts
312
+
313
+ except Exception as e:
314
+ self.logger.error(f"Failed to get artifact counts in batch: {e}")
315
+ # Fallback to 0 counts on error
316
+ return [(project, 0) for project in projects]
317
+
318
+ async def get_project_artifacts(self, db, project_id: str, user_id: str) -> List[ArtifactInfo]:
319
+ """
320
+ Get a list of artifacts for a given project.
321
+
322
+ Args:
323
+ db: The database session
324
+ project_id: The project ID
325
+ user_id: The requesting user ID
326
+
327
+ Returns:
328
+ List[ArtifactInfo]: A list of artifacts
329
+
330
+ Raises:
331
+ ValueError: If project not found or access denied
332
+ """
333
+ project = self.get_project(db, project_id, user_id)
334
+ if not project:
335
+ raise ValueError("Project not found or access denied")
336
+
337
+ if not self.artifact_service:
338
+ self.logger.warning(f"Attempted to get artifacts for project {project_id} but no artifact service is configured.")
339
+ return []
340
+
341
+ storage_user_id = project.user_id
342
+ storage_session_id = f"project-{project.id}"
343
+
344
+ self.logger.info(f"Fetching artifacts for project {project.id} with storage session {storage_session_id} and user {storage_user_id}")
345
+
346
+ artifacts = await get_artifact_info_list(
347
+ artifact_service=self.artifact_service,
348
+ app_name=self.app_name,
349
+ user_id=storage_user_id,
350
+ session_id=storage_session_id,
351
+ )
352
+ return artifacts
353
+
354
+ async def add_artifacts_to_project(
355
+ self,
356
+ db,
357
+ project_id: str,
358
+ user_id: str,
359
+ files: List[UploadFile],
360
+ file_metadata: Optional[dict] = None
361
+ ) -> List[dict]:
362
+ """
363
+ Add one or more artifacts to a project.
364
+
365
+ Args:
366
+ db: The database session
367
+ project_id: The project ID
368
+ user_id: The requesting user ID
369
+ files: List of files to add
370
+ file_metadata: Optional dictionary of metadata (e.g., descriptions)
371
+
372
+ Returns:
373
+ List[dict]: A list of results from the save operations
374
+
375
+ Raises:
376
+ ValueError: If project not found, access denied, or file size exceeds limit
377
+ """
378
+ log_prefix = f"[ProjectService:add_artifacts] Project {project_id}, User {user_id}:"
379
+
380
+ project = self.get_project(db, project_id, user_id)
381
+ if not project:
382
+ raise ValueError("Project not found or access denied")
383
+
384
+ if not self.artifact_service:
385
+ self.logger.warning(f"Attempted to add artifacts to project {project_id} but no artifact service is configured.")
386
+ raise ValueError("Artifact service is not configured")
387
+
388
+ if not files:
389
+ return []
390
+
391
+ # Validate file sizes before saving any artifacts
392
+ self.logger.info(f"{log_prefix} Validating {len(files)} files before adding to project")
393
+ validated_files = await self._validate_files(files, log_prefix)
394
+ self.logger.info(f"{log_prefix} All {len(files)} files passed size validation")
395
+
396
+ self.logger.info(f"Adding {len(validated_files)} artifacts to project {project_id} for user {user_id}")
397
+ storage_session_id = f"project-{project.id}"
398
+ results = []
399
+
400
+ for file, content_bytes in validated_files:
401
+ metadata = {"source": "project"}
402
+ if file_metadata and file.filename in file_metadata:
403
+ desc = file_metadata[file.filename]
404
+ if desc:
405
+ metadata["description"] = desc
406
+
407
+ result = await save_artifact_with_metadata(
408
+ artifact_service=self.artifact_service,
409
+ app_name=self.app_name,
410
+ user_id=project.user_id, # Always use project owner's ID for storage
411
+ session_id=storage_session_id,
412
+ filename=file.filename,
413
+ content_bytes=content_bytes,
414
+ mime_type=file.content_type,
415
+ metadata_dict=metadata,
416
+ timestamp=datetime.now(timezone.utc),
417
+ )
418
+ results.append(result)
419
+
420
+ self.logger.info(f"Finished adding {len(validated_files)} artifacts to project {project_id}")
421
+ return results
422
+
423
+ async def update_artifact_metadata(
424
+ self,
425
+ db,
426
+ project_id: str,
427
+ user_id: str,
428
+ filename: str,
429
+ description: Optional[str] = None
430
+ ) -> bool:
431
+ """
432
+ Update metadata (description) for a project artifact.
433
+
434
+ Args:
435
+ db: The database session
436
+ project_id: The project ID
437
+ user_id: The requesting user ID
438
+ filename: The filename of the artifact to update
439
+ description: New description for the artifact
440
+
441
+ Returns:
442
+ bool: True if update was successful, False if project not found
443
+
444
+ Raises:
445
+ ValueError: If user cannot modify the project or artifact service is missing
446
+ """
447
+ project = self.get_project(db, project_id, user_id)
448
+ if not project:
449
+ return False
450
+
451
+ if not self.artifact_service:
452
+ self.logger.warning(f"Attempted to update artifact metadata in project {project_id} but no artifact service is configured.")
453
+ raise ValueError("Artifact service is not configured")
454
+
455
+ storage_session_id = f"project-{project.id}"
456
+
457
+ self.logger.info(f"Updating metadata for artifact '{filename}' in project {project_id} for user {user_id}")
458
+
459
+ # Load the current artifact to get its content and existing metadata
460
+ try:
461
+ artifact_part = await self.artifact_service.load_artifact(
462
+ app_name=self.app_name,
463
+ user_id=project.user_id,
464
+ session_id=storage_session_id,
465
+ filename=filename,
466
+ )
467
+
468
+ if not artifact_part or not artifact_part.inline_data:
469
+ self.logger.warning(f"Artifact '{filename}' not found in project {project_id}")
470
+ return False
471
+
472
+ # Prepare updated metadata
473
+ metadata = {"source": "project"}
474
+ if description is not None:
475
+ metadata["description"] = description
476
+
477
+ # Save the artifact with updated metadata
478
+ await save_artifact_with_metadata(
479
+ artifact_service=self.artifact_service,
480
+ app_name=self.app_name,
481
+ user_id=project.user_id,
482
+ session_id=storage_session_id,
483
+ filename=filename,
484
+ content_bytes=artifact_part.inline_data.data,
485
+ mime_type=artifact_part.inline_data.mime_type,
486
+ metadata_dict=metadata,
487
+ timestamp=datetime.now(timezone.utc),
488
+ )
489
+
490
+ self.logger.info(f"Successfully updated metadata for artifact '{filename}' in project {project_id}")
491
+ return True
492
+
493
+ except Exception as e:
494
+ self.logger.error(f"Error updating artifact metadata: {e}")
495
+ raise
496
+
497
+ async def delete_artifact_from_project(self, db, project_id: str, user_id: str, filename: str) -> bool:
498
+ """
499
+ Deletes an artifact from a project.
500
+
501
+ Args:
502
+ db: The database session
503
+ project_id: The project ID
504
+ user_id: The requesting user ID
505
+ filename: The filename of the artifact to delete
506
+
507
+ Returns:
508
+ bool: True if deletion was attempted, False if project not found
509
+
510
+ Raises:
511
+ ValueError: If user cannot modify the project or artifact service is missing
512
+ """
513
+ project = self.get_project(db, project_id, user_id)
514
+ if not project:
515
+ return False
516
+
517
+ if not self.artifact_service:
518
+ self.logger.warning(f"Attempted to delete artifact from project {project_id} but no artifact service is configured.")
519
+ raise ValueError("Artifact service is not configured")
520
+
521
+ storage_session_id = f"project-{project.id}"
522
+
523
+ self.logger.info(f"Deleting artifact '{filename}' from project {project_id} for user {user_id}")
524
+
525
+ await self.artifact_service.delete_artifact(
526
+ app_name=self.app_name,
527
+ user_id=project.user_id, # Always use project owner's ID for storage
528
+ session_id=storage_session_id,
529
+ filename=filename,
530
+ )
531
+ return True
532
+
533
+ def update_project(self, db, project_id: str, user_id: str,
534
+ name: Optional[str] = None, description: Optional[str] = None,
535
+ system_prompt: Optional[str] = None, default_agent_id: Optional[str] = ...) -> Optional[Project]:
536
+ """
537
+ Update a project's details.
538
+
539
+ Args:
540
+ db: Database session
541
+ project_id: The project ID
542
+ user_id: The requesting user ID
543
+ name: New project name (optional)
544
+ description: New project description (optional)
545
+ system_prompt: New system prompt (optional)
546
+ default_agent_id: New default agent ID (optional, use ... sentinel to indicate not provided)
547
+
548
+ Returns:
549
+ Optional[Project]: The updated project if successful, None otherwise
550
+ """
551
+ # Validate business rules
552
+ if name is not None and name is not ... and not name.strip():
553
+ raise ValueError("Project name cannot be empty")
554
+
555
+ # Build update data
556
+ update_data = {}
557
+ if name is not None and name is not ...:
558
+ update_data["name"] = name.strip()
559
+ if description is not None and description is not ...:
560
+ update_data["description"] = description.strip() if description else None
561
+ if system_prompt is not None and system_prompt is not ...:
562
+ update_data["system_prompt"] = system_prompt.strip() if system_prompt else None
563
+ if default_agent_id is not ...:
564
+ update_data["default_agent_id"] = default_agent_id
565
+
566
+ if not update_data:
567
+ # Nothing to update - get existing project
568
+ return self.get_project(db, project_id, user_id)
569
+
570
+ project_repository = self._get_repositories(db)
571
+ self.logger.info(f"Updating project {project_id} for user {user_id}")
572
+ updated_project = project_repository.update(project_id, user_id, update_data)
573
+
574
+ if updated_project:
575
+ self.logger.info(f"Successfully updated project {project_id}")
576
+
577
+ return updated_project
578
+
579
+ def delete_project(self, db, project_id: str, user_id: str) -> bool:
580
+ """
581
+ Delete a project.
582
+
583
+ Args:
584
+ db: Database session
585
+ project_id: The project ID
586
+ user_id: The requesting user ID
587
+
588
+ Returns:
589
+ bool: True if deleted successfully, False otherwise
590
+ """
591
+ # First verify the project exists and user has access
592
+ existing_project = self.get_project(db, project_id, user_id)
593
+ if not existing_project:
594
+ return False
595
+
596
+ project_repository = self._get_repositories(db)
597
+ self.logger.info(f"Deleting project {project_id} for user {user_id}")
598
+ success = project_repository.delete(project_id, user_id)
599
+
600
+ if success:
601
+ self.logger.info(f"Successfully deleted project {project_id}")
602
+
603
+ return success
604
+
605
+ def soft_delete_project(self, db, project_id: str, user_id: str) -> bool:
606
+ """
607
+ Soft delete a project (mark as deleted without removing from database).
608
+ Also cascades soft delete to all sessions associated with this project.
609
+
610
+ Args:
611
+ db: Database session
612
+ project_id: The project ID
613
+ user_id: The requesting user ID
614
+
615
+ Returns:
616
+ bool: True if soft deleted successfully, False otherwise
617
+ """
618
+ # First verify the project exists and user has access
619
+ existing_project = self.get_project(db, project_id, user_id)
620
+ if not existing_project:
621
+ self.logger.warning(f"Attempted to soft delete non-existent project {project_id} by user {user_id}")
622
+ return False
623
+
624
+ self.logger.info(f"Soft deleting project {project_id} and its associated sessions for user {user_id}")
625
+
626
+ project_repository = self._get_repositories(db)
627
+ # Soft delete the project
628
+ success = project_repository.soft_delete(project_id, user_id)
629
+
630
+ if success:
631
+ from ..repository.session_repository import SessionRepository
632
+ session_repo = SessionRepository()
633
+ deleted_count = session_repo.soft_delete_by_project(db, project_id, user_id)
634
+ self.logger.info(f"Successfully soft deleted project {project_id} and {deleted_count} associated sessions")
635
+
636
+ return success
637
+
638
+ async def export_project_as_zip(
639
+ self, db, project_id: str, user_id: str
640
+ ) -> BytesIO:
641
+ """
642
+ Create ZIP file with project data and artifacts.
643
+ Returns in-memory ZIP file.
644
+
645
+ Args:
646
+ db: Database session
647
+ project_id: The project ID
648
+ user_id: The requesting user ID
649
+
650
+ Returns:
651
+ BytesIO: In-memory ZIP file
652
+
653
+ Raises:
654
+ ValueError: If project not found or access denied
655
+ """
656
+ # Get project
657
+ project = self.get_project(db, project_id, user_id)
658
+ if not project:
659
+ raise ValueError("Project not found or access denied")
660
+
661
+ # Get artifacts
662
+ artifacts = await self.get_project_artifacts(db, project_id, user_id)
663
+
664
+ # Calculate total size
665
+ total_size = sum(artifact.size for artifact in artifacts)
666
+
667
+ # Create export metadata
668
+ from ..routers.dto.project_dto import (
669
+ ProjectExportFormat,
670
+ ProjectExportData,
671
+ ProjectExportMetadata,
672
+ ArtifactMetadata,
673
+ )
674
+
675
+ export_data = ProjectExportFormat(
676
+ version="1.0",
677
+ exported_at=int(datetime.now(timezone.utc).timestamp() * 1000),
678
+ project=ProjectExportData(
679
+ name=project.name,
680
+ description=project.description,
681
+ system_prompt=project.system_prompt,
682
+ default_agent_id=project.default_agent_id,
683
+ metadata=ProjectExportMetadata(
684
+ original_created_at=project.created_at,
685
+ artifact_count=len(artifacts),
686
+ total_size_bytes=total_size,
687
+ ),
688
+ ),
689
+ artifacts=[
690
+ ArtifactMetadata(
691
+ filename=artifact.filename,
692
+ mime_type=artifact.mime_type or "application/octet-stream",
693
+ size=artifact.size,
694
+ metadata={
695
+ "description": artifact.description,
696
+ "source": artifact.source,
697
+ } if artifact.description or artifact.source else {},
698
+ )
699
+ for artifact in artifacts
700
+ ],
701
+ )
702
+
703
+ # Create ZIP in memory
704
+ zip_buffer = BytesIO()
705
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
706
+ # Add project.json
707
+ project_json = export_data.model_dump(by_alias=True, mode='json')
708
+ zip_file.writestr('project.json', json.dumps(project_json, indent=2))
709
+
710
+ # Add artifacts
711
+ if self.artifact_service and artifacts:
712
+ storage_session_id = f"project-{project.id}"
713
+ for artifact in artifacts:
714
+ try:
715
+ # Load artifact content
716
+ artifact_part = await self.artifact_service.load_artifact(
717
+ app_name=self.app_name,
718
+ user_id=project.user_id,
719
+ session_id=storage_session_id,
720
+ filename=artifact.filename,
721
+ )
722
+
723
+ if artifact_part and artifact_part.inline_data:
724
+ # Add to ZIP under artifacts/ directory
725
+ zip_file.writestr(
726
+ f'artifacts/{artifact.filename}',
727
+ artifact_part.inline_data.data
728
+ )
729
+ except Exception as e:
730
+ self.logger.warning(
731
+ f"Failed to add artifact {artifact.filename} to export: {e}"
732
+ )
733
+
734
+ zip_buffer.seek(0)
735
+ return zip_buffer
736
+
737
+ async def import_project_from_zip(
738
+ self, db, zip_file: UploadFile, user_id: str,
739
+ preserve_name: bool = False, custom_name: Optional[str] = None
740
+ ) -> tuple[Project, int, List[str]]:
741
+ """
742
+ Import project from ZIP file.
743
+
744
+ Args:
745
+ db: Database session
746
+ zip_file: Uploaded ZIP file
747
+ user_id: The importing user ID
748
+ preserve_name: Whether to preserve original name
749
+ custom_name: Custom name to use (overrides preserve_name)
750
+
751
+ Returns:
752
+ tuple: (created_project, artifacts_count, warnings)
753
+
754
+ Raises:
755
+ ValueError: If ZIP is invalid, import fails, or file size exceeds limit
756
+ """
757
+ log_prefix = f"[ProjectService:import_project] User {user_id}:"
758
+ warnings = []
759
+
760
+ # Read ZIP file content with size validation
761
+ self.logger.info(f"{log_prefix} Reading ZIP file")
762
+ zip_content = await zip_file.read()
763
+ zip_size = len(zip_content)
764
+ self.logger.info(f"{log_prefix} ZIP file read: {zip_size:,} bytes")
765
+
766
+ # Validate ZIP file size (separate, larger limit than individual artifacts)
767
+ if zip_size > self.max_zip_upload_size_bytes:
768
+ max_size_mb = self.max_zip_upload_size_bytes / (1024 * 1024)
769
+ file_size_mb = zip_size / (1024 * 1024)
770
+ error_msg = (
771
+ f"ZIP file '{zip_file.filename}' rejected: size ({file_size_mb:.2f} MB) "
772
+ f"exceeds maximum allowed ({max_size_mb:.2f} MB)"
773
+ )
774
+ self.logger.warning(f"{log_prefix} {error_msg}")
775
+ raise ValueError(error_msg)
776
+
777
+ zip_buffer = BytesIO(zip_content)
778
+
779
+ try:
780
+ with zipfile.ZipFile(zip_buffer, 'r') as zip_ref:
781
+ # Validate ZIP structure
782
+ if 'project.json' not in zip_ref.namelist():
783
+ raise ValueError("Invalid project export: missing project.json")
784
+
785
+ # Parse project.json
786
+ project_json_content = zip_ref.read('project.json').decode('utf-8')
787
+ project_data = json.loads(project_json_content)
788
+
789
+ # Validate version
790
+ if project_data.get('version') != '1.0':
791
+ raise ValueError(
792
+ f"Unsupported export version: {project_data.get('version')}"
793
+ )
794
+
795
+ # Determine project name
796
+ original_name = project_data['project']['name']
797
+ if custom_name:
798
+ desired_name = custom_name
799
+ elif preserve_name:
800
+ desired_name = original_name
801
+ else:
802
+ desired_name = original_name
803
+
804
+ # Resolve name conflicts
805
+ final_name = self._resolve_project_name_conflict(db, desired_name, user_id)
806
+ if final_name != desired_name:
807
+ warnings.append(
808
+ f"Name conflict resolved: '{desired_name}' → '{final_name}'"
809
+ )
810
+
811
+ # Get default agent ID, but set to None if not provided
812
+ # The agent may not exist in the target environment
813
+ imported_agent_id = project_data['project'].get('defaultAgentId')
814
+
815
+ # Create project (agent validation happens in create_project if needed)
816
+ project = await self.create_project(
817
+ db=db,
818
+ name=final_name,
819
+ user_id=user_id,
820
+ description=project_data['project'].get('description'),
821
+ system_prompt=project_data['project'].get('systemPrompt'),
822
+ default_agent_id=imported_agent_id,
823
+ )
824
+
825
+ # Add warning if agent was specified but may not exist
826
+ if imported_agent_id:
827
+ warnings.append(
828
+ f"Default agent '{imported_agent_id}' was imported. "
829
+ "Verify it exists in your environment."
830
+ )
831
+
832
+ # Import artifacts
833
+ artifacts_imported = 0
834
+ if self.artifact_service:
835
+ storage_session_id = f"project-{project.id}"
836
+ artifact_files = [
837
+ name for name in zip_ref.namelist()
838
+ if name.startswith('artifacts/') and name != 'artifacts/'
839
+ ]
840
+
841
+ for artifact_path in artifact_files:
842
+ try:
843
+ filename = artifact_path.replace('artifacts/', '')
844
+ content_bytes = zip_ref.read(artifact_path)
845
+
846
+ # Skip oversized artifacts with a warning (don't fail the entire import)
847
+ if len(content_bytes) > self.max_upload_size_bytes:
848
+ max_size_mb = self.max_upload_size_bytes / (1024 * 1024)
849
+ file_size_mb = len(content_bytes) / (1024 * 1024)
850
+ skip_msg = (
851
+ f"Skipped '{filename}': size ({file_size_mb:.2f} MB) "
852
+ f"exceeds maximum allowed ({max_size_mb:.2f} MB)"
853
+ )
854
+ self.logger.warning(f"{log_prefix} {skip_msg}")
855
+ warnings.append(skip_msg)
856
+ continue # Skip this artifact, continue with others
857
+
858
+ # Find metadata from project.json
859
+ artifact_meta = next(
860
+ (a for a in project_data.get('artifacts', [])
861
+ if a['filename'] == filename),
862
+ None
863
+ )
864
+
865
+ metadata = artifact_meta.get('metadata', {}) if artifact_meta else {}
866
+ mime_type = artifact_meta.get('mimeType', 'application/octet-stream') if artifact_meta else 'application/octet-stream'
867
+
868
+ # Save artifact
869
+ from ....agent.utils.artifact_helpers import save_artifact_with_metadata
870
+ await save_artifact_with_metadata(
871
+ artifact_service=self.artifact_service,
872
+ app_name=self.app_name,
873
+ user_id=project.user_id,
874
+ session_id=storage_session_id,
875
+ filename=filename,
876
+ content_bytes=content_bytes,
877
+ mime_type=mime_type,
878
+ metadata_dict=metadata,
879
+ timestamp=datetime.now(timezone.utc),
880
+ )
881
+ artifacts_imported += 1
882
+ except Exception as e:
883
+ self.logger.warning(
884
+ f"Failed to import artifact {artifact_path}: {e}"
885
+ )
886
+ warnings.append(f"Failed to import artifact: {filename}")
887
+
888
+ self.logger.info(
889
+ f"Successfully imported project {project.id} with {artifacts_imported} artifacts"
890
+ )
891
+ return project, artifacts_imported, warnings
892
+
893
+ except zipfile.BadZipFile:
894
+ raise ValueError("Invalid ZIP file")
895
+ except json.JSONDecodeError:
896
+ raise ValueError("Invalid project.json format")
897
+ except KeyError as e:
898
+ raise ValueError(f"Missing required field in project.json: {e}")
899
+
900
+ def _resolve_project_name_conflict(
901
+ self, db, desired_name: str, user_id: str
902
+ ) -> str:
903
+ """
904
+ Resolve project name conflicts by appending (2), (3), etc.
905
+ Similar to prompt import conflict resolution.
906
+
907
+ Args:
908
+ db: Database session
909
+ desired_name: The desired project name
910
+ user_id: The user ID
911
+
912
+ Returns:
913
+ str: A unique project name
914
+ """
915
+ project_repository = self._get_repositories(db)
916
+ existing_projects = project_repository.get_user_projects(user_id)
917
+ existing_names = {p.name.lower() for p in existing_projects}
918
+
919
+ if desired_name.lower() not in existing_names:
920
+ return desired_name
921
+
922
+ # Try appending (2), (3), etc.
923
+ counter = 2
924
+ while True:
925
+ candidate = f"{desired_name} ({counter})"
926
+ if candidate.lower() not in existing_names:
927
+ return candidate
928
+ counter += 1
929
+ if counter > 100: # Safety limit
930
+ raise ValueError("Unable to resolve name conflict")