solace-agent-mesh 1.6.3__py3-none-any.whl → 1.7.0__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 (244) hide show
  1. solace_agent_mesh/agent/adk/adk_llm.txt +12 -18
  2. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +1 -1
  3. solace_agent_mesh/agent/adk/callbacks.py +138 -20
  4. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +2 -0
  5. solace_agent_mesh/agent/adk/models/lite_llm.py +38 -5
  6. solace_agent_mesh/agent/adk/models/models_llm.txt +82 -35
  7. solace_agent_mesh/agent/adk/runner.py +9 -0
  8. solace_agent_mesh/agent/adk/stream_parser.py +6 -1
  9. solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
  10. solace_agent_mesh/agent/agent_llm.txt +61 -70
  11. solace_agent_mesh/agent/protocol/event_handlers.py +29 -1
  12. solace_agent_mesh/agent/protocol/protocol_llm.txt +1 -1
  13. solace_agent_mesh/agent/proxies/a2a/a2a_llm.txt +190 -0
  14. solace_agent_mesh/agent/proxies/base/base_llm.txt +148 -0
  15. solace_agent_mesh/agent/proxies/proxies_llm.txt +283 -0
  16. solace_agent_mesh/agent/sac/app.py +22 -0
  17. solace_agent_mesh/agent/sac/component.py +76 -40
  18. solace_agent_mesh/agent/sac/sac_llm.txt +1 -1
  19. solace_agent_mesh/agent/sac/task_execution_context.py +21 -0
  20. solace_agent_mesh/agent/testing/testing_llm.txt +2 -1
  21. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +13 -148
  22. solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
  23. solace_agent_mesh/agent/tools/tools_llm.txt +93 -80
  24. solace_agent_mesh/agent/tools/tools_llm_detail.txt +3 -2
  25. solace_agent_mesh/agent/utils/artifact_helpers.py +4 -0
  26. solace_agent_mesh/agent/utils/utils_llm.txt +16 -2
  27. solace_agent_mesh/assets/docs/404.html +3 -3
  28. solace_agent_mesh/assets/docs/assets/js/05749d90.c70b2be9.js +1 -0
  29. solace_agent_mesh/assets/docs/assets/js/15ba94aa.92fea363.js +1 -0
  30. solace_agent_mesh/assets/docs/assets/js/15e40e79.36003774.js +1 -0
  31. solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
  32. solace_agent_mesh/assets/docs/assets/js/3ac1795d.e4870a49.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/3ff0015d.b63ee53a.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/547e15cc.2f7790c1.js +1 -0
  35. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.45b32c2b.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
  37. solace_agent_mesh/assets/docs/assets/js/64195356.c498c4d0.js +1 -0
  38. solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
  39. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
  40. solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
  41. solace_agent_mesh/assets/docs/assets/js/8024126c.fa0e7186.js +1 -0
  42. solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
  43. solace_agent_mesh/assets/docs/assets/js/94e8668d.09ed9234.js +1 -0
  44. solace_agent_mesh/assets/docs/assets/js/{ab9708a8.3e6dd091.js → ab9708a8.245ae0ef.js} +1 -1
  45. solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
  46. solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.f902fad8.js +1 -0
  47. solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
  48. solace_agent_mesh/assets/docs/assets/js/db5d6442.3daf1696.js +1 -0
  49. solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
  50. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
  51. solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
  52. solace_agent_mesh/assets/docs/assets/js/e04b235d.c9c50c7b.js +1 -0
  53. solace_agent_mesh/assets/docs/assets/js/e3d9abda.d11c67a7.js +1 -0
  54. solace_agent_mesh/assets/docs/assets/js/{e6f9706b.e74a984d.js → e6f9706b.045d0fa1.js} +1 -1
  55. solace_agent_mesh/assets/docs/assets/js/e92d0134.3bda61dd.js +1 -0
  56. solace_agent_mesh/assets/docs/assets/js/f284c35a.5099c51e.js +1 -0
  57. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
  58. solace_agent_mesh/assets/docs/assets/js/main.e6488e8b.js +2 -0
  59. solace_agent_mesh/assets/docs/assets/js/runtime~main.d9606d6a.js +1 -0
  60. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
  64. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +18 -4
  65. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
  66. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
  67. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +5 -5
  68. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
  69. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
  70. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
  71. solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +196 -0
  72. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +4 -4
  73. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +5 -5
  74. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +6 -7
  75. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +4 -4
  76. solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes-deployment/index.html +47 -0
  77. solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +4 -4
  78. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +4 -4
  79. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
  80. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +160 -169
  81. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
  82. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
  83. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +4 -4
  84. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +4 -4
  85. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
  86. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
  87. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +5 -5
  88. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +4 -4
  89. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
  90. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
  91. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
  92. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
  93. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
  94. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
  95. solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +59 -0
  96. solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +62 -0
  97. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +10 -6
  98. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +4 -4
  99. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +4 -4
  100. solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
  101. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +27 -4
  102. solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
  103. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +4 -4
  104. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +5 -4
  105. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  106. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  107. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +290 -0
  108. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +9 -9
  109. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +4 -4
  110. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +4 -4
  111. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +4 -4
  112. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +4 -4
  113. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
  114. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
  115. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  116. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  117. solace_agent_mesh/assets/docs/lunr-index-1762189824009.json +1 -0
  118. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  119. solace_agent_mesh/assets/docs/search-doc-1762189824009.json +1 -0
  120. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  121. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  122. solace_agent_mesh/cli/__init__.py +1 -1
  123. solace_agent_mesh/cli/commands/docs_cmd.py +4 -1
  124. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +1 -1
  125. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-D4_RMYRh.js → authCallback-tcIFZLis.js} +1 -1
  126. solace_agent_mesh/client/webui/frontend/static/assets/{client-UZ3qU6Bq.js → client-CRYdKo2Q.js} +3 -3
  127. solace_agent_mesh/client/webui/frontend/static/assets/main-CojeY_1w.css +1 -0
  128. solace_agent_mesh/client/webui/frontend/static/assets/main-ILja9MCG.js +353 -0
  129. solace_agent_mesh/client/webui/frontend/static/assets/vendor-CINwxvwV.js +470 -0
  130. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  131. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  132. solace_agent_mesh/common/a2a/a2a_llm.txt +13 -20
  133. solace_agent_mesh/common/a2a/protocol.py +5 -0
  134. solace_agent_mesh/common/a2a/types.py +1 -0
  135. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +49 -11
  136. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +23 -6
  137. solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
  138. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +26 -9
  139. solace_agent_mesh/common/common_llm.txt +13 -34
  140. solace_agent_mesh/common/data_parts.py +20 -4
  141. solace_agent_mesh/common/middleware/middleware_llm.txt +1 -1
  142. solace_agent_mesh/common/sac/sac_llm.txt +1 -1
  143. solace_agent_mesh/common/sam_events/sam_events_llm.txt +1 -1
  144. solace_agent_mesh/common/services/employee_service.py +1 -1
  145. solace_agent_mesh/common/services/providers/providers_llm.txt +3 -2
  146. solace_agent_mesh/common/services/services_llm.txt +9 -4
  147. solace_agent_mesh/common/utils/embeds/constants.py +1 -0
  148. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +1 -1
  149. solace_agent_mesh/common/utils/embeds/modifiers.py +2 -1
  150. solace_agent_mesh/common/utils/embeds/resolver.py +58 -6
  151. solace_agent_mesh/common/utils/embeds/types.py +8 -0
  152. solace_agent_mesh/common/utils/utils_llm.txt +5 -6
  153. solace_agent_mesh/core_a2a/core_a2a_llm.txt +1 -1
  154. solace_agent_mesh/gateway/adapter/__init__.py +1 -0
  155. solace_agent_mesh/gateway/adapter/base.py +143 -0
  156. solace_agent_mesh/gateway/adapter/types.py +221 -0
  157. solace_agent_mesh/gateway/base/app.py +29 -2
  158. solace_agent_mesh/gateway/base/base_llm.txt +10 -8
  159. solace_agent_mesh/gateway/base/component.py +573 -142
  160. solace_agent_mesh/gateway/gateway_llm.txt +55 -59
  161. solace_agent_mesh/gateway/generic/__init__.py +1 -0
  162. solace_agent_mesh/gateway/generic/app.py +50 -0
  163. solace_agent_mesh/gateway/generic/component.py +650 -0
  164. solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +99 -49
  165. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_fulltext_search_indexes.py +92 -0
  166. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
  167. solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +150 -0
  168. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
  169. solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
  170. solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +26 -20
  171. solace_agent_mesh/gateway/http_sse/app.py +0 -14
  172. solace_agent_mesh/gateway/http_sse/component.py +17 -56
  173. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +1 -1
  174. solace_agent_mesh/gateway/http_sse/dependencies.py +21 -3
  175. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +8 -8
  176. solace_agent_mesh/gateway/http_sse/main.py +23 -5
  177. solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
  178. solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +56 -98
  179. solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
  180. solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
  181. solace_agent_mesh/gateway/http_sse/repository/entities/session.py +23 -1
  182. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
  183. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +112 -4
  184. solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +9 -1
  185. solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +51 -60
  186. solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
  187. solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
  188. solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +7 -1
  189. solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
  190. solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
  191. solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +125 -157
  192. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +269 -8
  193. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +143 -51
  194. solace_agent_mesh/gateway/http_sse/routers/config.py +69 -0
  195. solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +198 -94
  196. solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
  197. solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +68 -18
  198. solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +13 -0
  199. solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +30 -0
  200. solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +51 -35
  201. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +2 -0
  202. solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
  203. solace_agent_mesh/gateway/http_sse/routers/projects.py +542 -0
  204. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +9 -11
  205. solace_agent_mesh/gateway/http_sse/routers/sessions.py +154 -3
  206. solace_agent_mesh/gateway/http_sse/routers/tasks.py +296 -4
  207. solace_agent_mesh/gateway/http_sse/services/project_service.py +403 -0
  208. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +16 -10
  209. solace_agent_mesh/gateway/http_sse/services/session_service.py +178 -6
  210. solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +2 -3
  211. solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +48 -14
  212. solace_agent_mesh/solace_agent_mesh_llm.txt +1 -1
  213. {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.0.dist-info}/METADATA +3 -5
  214. {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.0.dist-info}/RECORD +218 -175
  215. solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.js +0 -1
  216. solace_agent_mesh/assets/docs/assets/js/3ac1795d.76654dd9.js +0 -1
  217. solace_agent_mesh/assets/docs/assets/js/3ff0015d.2be20244.js +0 -1
  218. solace_agent_mesh/assets/docs/assets/js/547e15cc.2cbb060a.js +0 -1
  219. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +0 -1
  220. solace_agent_mesh/assets/docs/assets/js/631738c7.7c4594c9.js +0 -1
  221. solace_agent_mesh/assets/docs/assets/js/6a520c9d.ba015d81.js +0 -1
  222. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +0 -1
  223. solace_agent_mesh/assets/docs/assets/js/71da7b71.ddbdfbe2.js +0 -1
  224. solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
  225. solace_agent_mesh/assets/docs/assets/js/94e8668d.3b883666.js +0 -1
  226. solace_agent_mesh/assets/docs/assets/js/da0b5bad.d08a9466.js +0 -1
  227. solace_agent_mesh/assets/docs/assets/js/dd817ffc.0aa9630a.js +0 -1
  228. solace_agent_mesh/assets/docs/assets/js/dd81e2b8.d590bc9e.js +0 -1
  229. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +0 -1
  230. solace_agent_mesh/assets/docs/assets/js/e3d9abda.6b9493d0.js +0 -1
  231. solace_agent_mesh/assets/docs/assets/js/e92d0134.4f395c6b.js +0 -1
  232. solace_agent_mesh/assets/docs/assets/js/f284c35a.720d2ef2.js +0 -1
  233. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
  234. solace_agent_mesh/assets/docs/assets/js/main.ed05b14d.js +0 -2
  235. solace_agent_mesh/assets/docs/assets/js/runtime~main.a8a75e0b.js +0 -1
  236. solace_agent_mesh/assets/docs/lunr-index-1761744323675.json +0 -1
  237. solace_agent_mesh/assets/docs/search-doc-1761744323675.json +0 -1
  238. solace_agent_mesh/client/webui/frontend/static/assets/main--3yJYl7S.css +0 -1
  239. solace_agent_mesh/client/webui/frontend/static/assets/main-DojKHS49.js +0 -342
  240. solace_agent_mesh/client/webui/frontend/static/assets/vendor-DSqhjwq_.js +0 -405
  241. /solace_agent_mesh/assets/docs/assets/js/{main.ed05b14d.js.LICENSE.txt → main.e6488e8b.js.LICENSE.txt} +0 -0
  242. {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.0.dist-info}/WHEEL +0 -0
  243. {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.0.dist-info}/entry_points.txt +0 -0
  244. {solace_agent_mesh-1.6.3.dist-info → solace_agent_mesh-1.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,403 @@
1
+ """
2
+ Business service for project-related operations.
3
+ """
4
+
5
+ from typing import List, Optional, TYPE_CHECKING
6
+ import logging
7
+ from fastapi import UploadFile
8
+ from datetime import datetime, timezone
9
+
10
+ from ....agent.utils.artifact_helpers import get_artifact_info_list, save_artifact_with_metadata
11
+
12
+ try:
13
+ from google.adk.artifacts import BaseArtifactService
14
+ except ImportError:
15
+
16
+ class BaseArtifactService:
17
+ pass
18
+
19
+
20
+ from ....common.a2a.types import ArtifactInfo
21
+ from ..repository.interfaces import IProjectRepository
22
+ from ..repository.entities.project import Project
23
+
24
+ if TYPE_CHECKING:
25
+ from ..component import WebUIBackendComponent
26
+
27
+
28
+ class ProjectService:
29
+ """Service layer for project business logic."""
30
+
31
+ def __init__(
32
+ self,
33
+ component: "WebUIBackendComponent" = None,
34
+ ):
35
+ self.component = component
36
+ self.artifact_service = component.get_shared_artifact_service() if component else None
37
+ self.app_name = component.get_config("name", "WebUIBackendApp") if component else "WebUIBackendApp"
38
+ self.logger = logging.getLogger(__name__)
39
+
40
+ def _get_repositories(self, db):
41
+ """Create project repository for the given database session."""
42
+ from ..repository.project_repository import ProjectRepository
43
+ return ProjectRepository(db)
44
+
45
+ def is_persistence_enabled(self) -> bool:
46
+ """Checks if the service is configured with a persistent backend."""
47
+ return self.component and self.component.database_url is not None
48
+
49
+ async def create_project(
50
+ self,
51
+ db,
52
+ name: str,
53
+ user_id: str,
54
+ description: Optional[str] = None,
55
+ system_prompt: Optional[str] = None,
56
+ default_agent_id: Optional[str] = None,
57
+ files: Optional[List[UploadFile]] = None,
58
+ file_metadata: Optional[dict] = None,
59
+ ) -> Project:
60
+ """
61
+ Create a new project for a user.
62
+
63
+ Args:
64
+ db: Database session
65
+ name: Project name
66
+ user_id: ID of the user creating the project
67
+ description: Optional project description
68
+ system_prompt: Optional system prompt
69
+ default_agent_id: Optional default agent ID for new chats
70
+ files: Optional list of files to associate with the project
71
+
72
+ Returns:
73
+ DomainProject: The created project
74
+
75
+ Raises:
76
+ ValueError: If project name is invalid or user_id is missing
77
+ """
78
+ self.logger.info(f"Creating new project '{name}' for user {user_id}")
79
+
80
+ # Business validation
81
+ if not name or not name.strip():
82
+ raise ValueError("Project name cannot be empty")
83
+
84
+ if not user_id:
85
+ raise ValueError("User ID is required to create a project")
86
+
87
+ project_repository = self._get_repositories(db)
88
+
89
+ # Check for duplicate project name for this user
90
+ existing_projects = project_repository.get_user_projects(user_id)
91
+ if any(p.name.lower() == name.strip().lower() for p in existing_projects):
92
+ raise ValueError(f"A project with the name '{name.strip()}' already exists")
93
+
94
+ # Create the project
95
+ project_domain = project_repository.create_project(
96
+ name=name.strip(),
97
+ user_id=user_id,
98
+ description=description.strip() if description else None,
99
+ system_prompt=system_prompt.strip() if system_prompt else None,
100
+ default_agent_id=default_agent_id,
101
+ )
102
+
103
+ if files and self.artifact_service:
104
+ self.logger.info(
105
+ f"Project {project_domain.id} created, now saving {len(files)} artifacts."
106
+ )
107
+ project_session_id = f"project-{project_domain.id}"
108
+ for file in files:
109
+ content_bytes = await file.read()
110
+ metadata = {"source": "project"}
111
+ if file_metadata and file.filename in file_metadata:
112
+ desc = file_metadata[file.filename]
113
+ if desc:
114
+ metadata["description"] = desc
115
+
116
+ await save_artifact_with_metadata(
117
+ artifact_service=self.artifact_service,
118
+ app_name=self.app_name,
119
+ user_id=project_domain.user_id,
120
+ session_id=project_session_id,
121
+ filename=file.filename,
122
+ content_bytes=content_bytes,
123
+ mime_type=file.content_type,
124
+ metadata_dict=metadata,
125
+ timestamp=datetime.now(timezone.utc),
126
+ )
127
+ self.logger.info(f"Saved {len(files)} artifacts for project {project_domain.id}")
128
+
129
+ self.logger.info(
130
+ f"Successfully created project {project_domain.id} for user {user_id}"
131
+ )
132
+ return project_domain
133
+
134
+ def get_project(self, db, project_id: str, user_id: str) -> Optional[Project]:
135
+ """
136
+ Get a project by ID, ensuring the user has access to it.
137
+
138
+ Args:
139
+ db: Database session
140
+ project_id: The project ID
141
+ user_id: The requesting user ID
142
+
143
+ Returns:
144
+ Optional[Project]: The project if found and accessible, None otherwise
145
+ """
146
+ project_repository = self._get_repositories(db)
147
+ return project_repository.get_by_id(project_id, user_id)
148
+
149
+ def get_user_projects(self, db, user_id: str) -> List[Project]:
150
+ """
151
+ Get all projects owned by a specific user.
152
+
153
+ Args:
154
+ db: Database session
155
+ user_id: The user ID
156
+
157
+ Returns:
158
+ List[DomainProject]: List of user's projects
159
+ """
160
+ self.logger.debug(f"Retrieving projects for user {user_id}")
161
+ project_repository = self._get_repositories(db)
162
+ db_projects = project_repository.get_user_projects(user_id)
163
+ return db_projects
164
+
165
+ async def get_project_artifacts(self, db, project_id: str, user_id: str) -> List[ArtifactInfo]:
166
+ """
167
+ Get a list of artifacts for a given project.
168
+
169
+ Args:
170
+ db: The database session
171
+ project_id: The project ID
172
+ user_id: The requesting user ID
173
+
174
+ Returns:
175
+ List[ArtifactInfo]: A list of artifacts
176
+
177
+ Raises:
178
+ ValueError: If project not found or access denied
179
+ """
180
+ project = self.get_project(db, project_id, user_id)
181
+ if not project:
182
+ raise ValueError("Project not found or access denied")
183
+
184
+ if not self.artifact_service:
185
+ self.logger.warning(f"Attempted to get artifacts for project {project_id} but no artifact service is configured.")
186
+ return []
187
+
188
+ storage_user_id = project.user_id
189
+ storage_session_id = f"project-{project.id}"
190
+
191
+ self.logger.info(f"Fetching artifacts for project {project.id} with storage session {storage_session_id} and user {storage_user_id}")
192
+
193
+ artifacts = await get_artifact_info_list(
194
+ artifact_service=self.artifact_service,
195
+ app_name=self.app_name,
196
+ user_id=storage_user_id,
197
+ session_id=storage_session_id,
198
+ )
199
+ return artifacts
200
+
201
+ async def add_artifacts_to_project(
202
+ self,
203
+ db,
204
+ project_id: str,
205
+ user_id: str,
206
+ files: List[UploadFile],
207
+ file_metadata: Optional[dict] = None
208
+ ) -> List[dict]:
209
+ """
210
+ Add one or more artifacts to a project.
211
+
212
+ Args:
213
+ db: The database session
214
+ project_id: The project ID
215
+ user_id: The requesting user ID
216
+ files: List of files to add
217
+ file_metadata: Optional dictionary of metadata (e.g., descriptions)
218
+
219
+ Returns:
220
+ List[dict]: A list of results from the save operations
221
+
222
+ Raises:
223
+ ValueError: If project not found or access denied
224
+ """
225
+ project = self.get_project(db, project_id, user_id)
226
+ if not project:
227
+ raise ValueError("Project not found or access denied")
228
+
229
+ if not self.artifact_service:
230
+ self.logger.warning(f"Attempted to add artifacts to project {project_id} but no artifact service is configured.")
231
+ raise ValueError("Artifact service is not configured")
232
+
233
+ if not files:
234
+ return []
235
+
236
+ self.logger.info(f"Adding {len(files)} artifacts to project {project_id} for user {user_id}")
237
+ storage_session_id = f"project-{project.id}"
238
+ results = []
239
+
240
+ for file in files:
241
+ content_bytes = await file.read()
242
+ metadata = {"source": "project"}
243
+ if file_metadata and file.filename in file_metadata:
244
+ desc = file_metadata[file.filename]
245
+ if desc:
246
+ metadata["description"] = desc
247
+
248
+ result = await save_artifact_with_metadata(
249
+ artifact_service=self.artifact_service,
250
+ app_name=self.app_name,
251
+ user_id=project.user_id, # Always use project owner's ID for storage
252
+ session_id=storage_session_id,
253
+ filename=file.filename,
254
+ content_bytes=content_bytes,
255
+ mime_type=file.content_type,
256
+ metadata_dict=metadata,
257
+ timestamp=datetime.now(timezone.utc),
258
+ )
259
+ results.append(result)
260
+
261
+ self.logger.info(f"Finished adding {len(files)} artifacts to project {project_id}")
262
+ return results
263
+
264
+ async def delete_artifact_from_project(self, db, project_id: str, user_id: str, filename: str) -> bool:
265
+ """
266
+ Deletes an artifact from a project.
267
+
268
+ Args:
269
+ db: The database session
270
+ project_id: The project ID
271
+ user_id: The requesting user ID
272
+ filename: The filename of the artifact to delete
273
+
274
+ Returns:
275
+ bool: True if deletion was attempted, False if project not found
276
+
277
+ Raises:
278
+ ValueError: If user cannot modify the project or artifact service is missing
279
+ """
280
+ project = self.get_project(db, project_id, user_id)
281
+ if not project:
282
+ return False
283
+
284
+ if not self.artifact_service:
285
+ self.logger.warning(f"Attempted to delete artifact from project {project_id} but no artifact service is configured.")
286
+ raise ValueError("Artifact service is not configured")
287
+
288
+ storage_session_id = f"project-{project.id}"
289
+
290
+ self.logger.info(f"Deleting artifact '{filename}' from project {project_id} for user {user_id}")
291
+
292
+ await self.artifact_service.delete_artifact(
293
+ app_name=self.app_name,
294
+ user_id=project.user_id, # Always use project owner's ID for storage
295
+ session_id=storage_session_id,
296
+ filename=filename,
297
+ )
298
+ return True
299
+
300
+ def update_project(self, db, project_id: str, user_id: str,
301
+ name: Optional[str] = None, description: Optional[str] = None,
302
+ system_prompt: Optional[str] = None, default_agent_id: Optional[str] = ...) -> Optional[Project]:
303
+ """
304
+ Update a project's details.
305
+
306
+ Args:
307
+ db: Database session
308
+ project_id: The project ID
309
+ user_id: The requesting user ID
310
+ name: New project name (optional)
311
+ description: New project description (optional)
312
+ system_prompt: New system prompt (optional)
313
+ default_agent_id: New default agent ID (optional, use ... sentinel to indicate not provided)
314
+
315
+ Returns:
316
+ Optional[Project]: The updated project if successful, None otherwise
317
+ """
318
+ # Validate business rules
319
+ if name is not None and name is not ... and not name.strip():
320
+ raise ValueError("Project name cannot be empty")
321
+
322
+ # Build update data
323
+ update_data = {}
324
+ if name is not None and name is not ...:
325
+ update_data["name"] = name.strip()
326
+ if description is not None and description is not ...:
327
+ update_data["description"] = description.strip() if description else None
328
+ if system_prompt is not None and system_prompt is not ...:
329
+ update_data["system_prompt"] = system_prompt.strip() if system_prompt else None
330
+ if default_agent_id is not ...:
331
+ update_data["default_agent_id"] = default_agent_id
332
+
333
+ if not update_data:
334
+ # Nothing to update - get existing project
335
+ return self.get_project(db, project_id, user_id)
336
+
337
+ project_repository = self._get_repositories(db)
338
+ self.logger.info(f"Updating project {project_id} for user {user_id}")
339
+ updated_project = project_repository.update(project_id, user_id, update_data)
340
+
341
+ if updated_project:
342
+ self.logger.info(f"Successfully updated project {project_id}")
343
+
344
+ return updated_project
345
+
346
+ def delete_project(self, db, project_id: str, user_id: str) -> bool:
347
+ """
348
+ Delete a project.
349
+
350
+ Args:
351
+ db: Database session
352
+ project_id: The project ID
353
+ user_id: The requesting user ID
354
+
355
+ Returns:
356
+ bool: True if deleted successfully, False otherwise
357
+ """
358
+ # First verify the project exists and user has access
359
+ existing_project = self.get_project(db, project_id, user_id)
360
+ if not existing_project:
361
+ return False
362
+
363
+ project_repository = self._get_repositories(db)
364
+ self.logger.info(f"Deleting project {project_id} for user {user_id}")
365
+ success = project_repository.delete(project_id, user_id)
366
+
367
+ if success:
368
+ self.logger.info(f"Successfully deleted project {project_id}")
369
+
370
+ return success
371
+
372
+ def soft_delete_project(self, db, project_id: str, user_id: str) -> bool:
373
+ """
374
+ Soft delete a project (mark as deleted without removing from database).
375
+ Also cascades soft delete to all sessions associated with this project.
376
+
377
+ Args:
378
+ db: Database session
379
+ project_id: The project ID
380
+ user_id: The requesting user ID
381
+
382
+ Returns:
383
+ bool: True if soft deleted successfully, False otherwise
384
+ """
385
+ # First verify the project exists and user has access
386
+ existing_project = self.get_project(db, project_id, user_id)
387
+ if not existing_project:
388
+ self.logger.warning(f"Attempted to soft delete non-existent project {project_id} by user {user_id}")
389
+ return False
390
+
391
+ self.logger.info(f"Soft deleting project {project_id} and its associated sessions for user {user_id}")
392
+
393
+ project_repository = self._get_repositories(db)
394
+ # Soft delete the project
395
+ success = project_repository.soft_delete(project_id, user_id)
396
+
397
+ if success:
398
+ from ..repository.session_repository import SessionRepository
399
+ session_repo = SessionRepository()
400
+ deleted_count = session_repo.soft_delete_by_project(db, project_id, user_id)
401
+ self.logger.info(f"Successfully soft deleted project {project_id} and {deleted_count} associated sessions")
402
+
403
+ return success
@@ -58,7 +58,7 @@ else:
58
58
  - `cleanup_old_data() -> None` - Main orchestration method for cleaning up old data, calls cleanup methods for tasks and feedback
59
59
 
60
60
  **Constants/Variables:**
61
- - `MIN_RETENTION_DAYS: int` - Minimum retention period (7 days)
61
+ - `MIN_RETENTION_DAYS: int` - Minimum retention period (1 day)
62
62
  - `MIN_CLEANUP_INTERVAL_HOURS: int` - Minimum cleanup interval (1 hour)
63
63
  - `MIN_BATCH_SIZE: int` - Minimum batch size for deletion (1)
64
64
  - `MAX_BATCH_SIZE: int` - Maximum batch size for deletion (10000)
@@ -160,16 +160,16 @@ asyncio.run(search_users())
160
160
  - `is_persistence_enabled() -> bool` - Checks if the service is configured with a persistent backend
161
161
  - `get_user_sessions(db: DbSession, user_id: UserId, pagination: PaginationParams | None = None) -> PaginatedResponse[Session]` - Retrieves paginated sessions for a user
162
162
  - `get_session_details(db: DbSession, session_id: SessionId, user_id: UserId) -> Session | None` - Gets session details for a specific session
163
- - `get_session_history(db: DbSession, session_id: SessionId, user_id: UserId, pagination: PaginationInfo | None = None) -> SessionHistory | None` - Gets session with messages
164
163
  - `create_session(db: DbSession, user_id: UserId, name: str | None = None, agent_id: str | None = None, session_id: str | None = None) -> Optional[Session]` - Creates a new session
165
164
  - `update_session_name(db: DbSession, session_id: SessionId, user_id: UserId, name: str) -> Session | None` - Updates session name
166
165
  - `delete_session_with_notifications(db: DbSession, session_id: SessionId, user_id: UserId) -> bool` - Deletes session and notifies agents
167
- - `add_message_to_session(db: DbSession, session_id: SessionId, user_id: UserId, message: str, sender_type: SenderType, sender_name: str, agent_id: str | None = None, message_type: MessageType = MessageType.TEXT) -> Message` - Adds a message to a session
166
+ - `save_task(db: DbSession, task_id: str, session_id: str, user_id: str, user_message: Optional[str], message_bubbles: str, task_metadata: Optional[str] = None) -> ChatTask` - Saves a complete task interaction
167
+ - `get_session_tasks(db: DbSession, session_id: str, user_id: str) -> List[ChatTask]` - Gets all tasks for a session
168
+ - `get_session_messages_from_tasks(db: DbSession, session_id: str, user_id: str) -> List[Dict[str, Any]]` - Gets session messages by flattening task message_bubbles for backward compatibility
168
169
 
169
170
  **Usage Examples:**
170
171
  ```python
171
172
  from solace_agent_mesh.gateway.http_sse.services.session_service import SessionService
172
- from solace_agent_mesh.gateway.http_sse.shared.enums import SenderType, MessageType
173
173
  from sqlalchemy.orm import Session as DbSession
174
174
 
175
175
  # Initialize with component
@@ -186,14 +186,20 @@ with your_session_factory() as db:
186
186
  agent_id="assistant-agent"
187
187
  )
188
188
 
189
- # Add a message to the session
190
- message = session_service.add_message_to_session(
189
+ # Save a task with message bubbles
190
+ import json
191
+ message_bubbles = json.dumps([
192
+ {"type": "user", "text": "Hello", "id": "msg1"},
193
+ {"type": "agent", "text": "Hi there!", "id": "msg2"}
194
+ ])
195
+
196
+ task = session_service.save_task(
191
197
  db=db,
198
+ task_id="task-123",
192
199
  session_id=session.id,
193
200
  user_id="user123",
194
- message="Hello, how can you help me?",
195
- sender_type=SenderType.USER,
196
- sender_name="John Doe"
201
+ user_message="Hello",
202
+ message_bubbles=message_bubbles
197
203
  )
198
204
 
199
205
  # Get user's sessions with pagination
@@ -294,4 +300,4 @@ async def cancel_task_example():
294
300
  asyncio.run(cancel_task_example())
295
301
  ```
296
302
 
297
- # content_hash: 12385f5117d2f5c30f0e44a0913ea91d62722aa82eb55268ae6bd317311ec5eb
303
+ # content_hash: 09bc3008d9a6c148b0b9d7477d20b26514f7fc4ee72f36eda5348f8f7d51f2ea
@@ -42,13 +42,19 @@ class SessionService:
42
42
  self,
43
43
  db: DbSession,
44
44
  user_id: UserId,
45
- pagination: PaginationParams | None = None
45
+ pagination: PaginationParams | None = None,
46
+ project_id: str | None = None
46
47
  ) -> PaginatedResponse[Session]:
47
48
  """
48
- Get paginated sessions for a user with full metadata.
49
-
49
+ Get paginated sessions for a user with full metadata including project names.
50
50
  Uses default pagination if none provided (page 1, size 20).
51
51
  Returns paginated response with pageNumber, pageSize, nextPage, totalPages, totalCount.
52
+
53
+ Args:
54
+ db: Database session
55
+ user_id: User ID to filter sessions by
56
+ pagination: Pagination parameters
57
+ project_id: Optional project ID to filter sessions by (for project-specific views)
52
58
  """
53
59
  if not user_id or user_id.strip() == "":
54
60
  raise ValueError("User ID cannot be empty")
@@ -56,9 +62,24 @@ class SessionService:
56
62
  pagination = get_pagination_or_default(pagination)
57
63
  session_repository = self._get_repositories(db)
58
64
 
59
- # Pass pagination params directly - repository will handle offset calculation
60
- sessions = session_repository.find_by_user(db, user_id, pagination)
61
- total_count = session_repository.count_by_user(db, user_id)
65
+ # Fetch sessions with optional project filtering
66
+ sessions = session_repository.find_by_user(db, user_id, pagination, project_id=project_id)
67
+ total_count = session_repository.count_by_user(db, user_id, project_id=project_id)
68
+
69
+ # Enrich sessions with project names
70
+ # Collect unique project IDs
71
+ project_ids = [s.project_id for s in sessions if s.project_id]
72
+
73
+ if project_ids:
74
+ # Fetch all projects in one query
75
+ from ..repository.models import ProjectModel
76
+ projects = db.query(ProjectModel).filter(ProjectModel.id.in_(project_ids)).all()
77
+ project_map = {p.id: p.name for p in projects}
78
+
79
+ # Map project names to sessions
80
+ for session in sessions:
81
+ if session.project_id:
82
+ session.project_name = project_map.get(session.project_id)
62
83
 
63
84
  return PaginatedResponse.create(sessions, total_count, pagination)
64
85
 
@@ -78,6 +99,7 @@ class SessionService:
78
99
  name: str | None = None,
79
100
  agent_id: str | None = None,
80
101
  session_id: str | None = None,
102
+ project_id: str | None = None,
81
103
  ) -> Optional[Session]:
82
104
  if not self.is_persistence_enabled():
83
105
  log.debug("Persistence is not enabled. Skipping session creation in DB.")
@@ -95,6 +117,7 @@ class SessionService:
95
117
  user_id=user_id,
96
118
  name=name,
97
119
  agent_id=agent_id,
120
+ project_id=project_id,
98
121
  created_time=now_ms,
99
122
  updated_time=now_ms,
100
123
  )
@@ -166,6 +189,155 @@ class SessionService:
166
189
 
167
190
  return True
168
191
 
192
+ def soft_delete_session(
193
+ self, db: DbSession, session_id: SessionId, user_id: UserId
194
+ ) -> bool:
195
+ """
196
+ Soft delete a session (mark as deleted without removing from database).
197
+
198
+ Args:
199
+ db: Database session
200
+ session_id: Session ID to soft delete
201
+ user_id: User ID performing the deletion
202
+
203
+ Returns:
204
+ bool: True if soft deleted successfully, False otherwise
205
+ """
206
+ if not self._is_valid_session_id(session_id):
207
+ raise ValueError("Invalid session ID")
208
+
209
+ session_repository = self._get_repositories(db)
210
+ session = session_repository.find_user_session(db, session_id, user_id)
211
+ if not session:
212
+ log.warning(
213
+ "Attempted to soft delete non-existent session %s by user %s",
214
+ session_id,
215
+ user_id,
216
+ )
217
+ return False
218
+
219
+ if not session.can_be_deleted_by_user(user_id):
220
+ log.warning(
221
+ "User %s not authorized to soft delete session %s", user_id, session_id
222
+ )
223
+ return False
224
+
225
+ deleted = session_repository.soft_delete(db, session_id, user_id)
226
+ if not deleted:
227
+ return False
228
+
229
+ log.info("Session %s soft deleted successfully by user %s", session_id, user_id)
230
+ return True
231
+
232
+ def move_session_to_project(
233
+ self, db: DbSession, session_id: SessionId, user_id: UserId, new_project_id: str | None
234
+ ) -> Session | None:
235
+ """
236
+ Move a session to a different project.
237
+
238
+ Args:
239
+ db: Database session
240
+ session_id: Session ID to move
241
+ user_id: User ID performing the move
242
+ new_project_id: New project ID (or None to remove from project)
243
+
244
+ Returns:
245
+ Session: Updated session if successful, None otherwise
246
+
247
+ Raises:
248
+ ValueError: If session or project validation fails
249
+ """
250
+ if not self._is_valid_session_id(session_id):
251
+ raise ValueError("Invalid session ID")
252
+
253
+ # Validate project exists and user has access if project_id is provided
254
+ if new_project_id:
255
+ from ..repository.models import ProjectModel
256
+ project = db.query(ProjectModel).filter(
257
+ ProjectModel.id == new_project_id,
258
+ ProjectModel.user_id == user_id,
259
+ ProjectModel.deleted_at.is_(None)
260
+ ).first()
261
+
262
+ if not project:
263
+ raise ValueError(f"Project {new_project_id} not found or access denied")
264
+
265
+ session_repository = self._get_repositories(db)
266
+ updated_session = session_repository.move_to_project(db, session_id, user_id, new_project_id)
267
+
268
+ if not updated_session:
269
+ log.warning(
270
+ "Failed to move session %s to project %s for user %s",
271
+ session_id,
272
+ new_project_id,
273
+ user_id,
274
+ )
275
+ return None
276
+
277
+ log.info(
278
+ "Session %s moved to project %s by user %s",
279
+ session_id,
280
+ new_project_id or "None",
281
+ user_id,
282
+ )
283
+ return updated_session
284
+
285
+ def search_sessions(
286
+ self,
287
+ db: DbSession,
288
+ user_id: UserId,
289
+ query: str,
290
+ pagination: PaginationParams | None = None,
291
+ project_id: str | None = None
292
+ ) -> PaginatedResponse[Session]:
293
+ """
294
+ Search sessions by name or content.
295
+
296
+ Args:
297
+ db: Database session
298
+ user_id: User ID to filter sessions by
299
+ query: Search query string
300
+ pagination: Pagination parameters
301
+ project_id: Optional project ID to filter sessions by
302
+
303
+ Returns:
304
+ PaginatedResponse[Session]: Paginated search results
305
+ """
306
+ if not user_id or user_id.strip() == "":
307
+ raise ValueError("User ID cannot be empty")
308
+
309
+ if not query or query.strip() == "":
310
+ raise ValueError("Search query cannot be empty")
311
+
312
+ pagination = get_pagination_or_default(pagination)
313
+ session_repository = self._get_repositories(db)
314
+
315
+ # Search sessions
316
+ sessions = session_repository.search(db, user_id, query.strip(), pagination, project_id)
317
+ total_count = session_repository.count_search_results(db, user_id, query.strip(), project_id)
318
+
319
+ # Enrich sessions with project names
320
+ project_ids = [s.project_id for s in sessions if s.project_id]
321
+
322
+ if project_ids:
323
+ from ..repository.models import ProjectModel
324
+ projects = db.query(ProjectModel).filter(ProjectModel.id.in_(project_ids)).all()
325
+ project_map = {p.id: p.name for p in projects}
326
+
327
+ for session in sessions:
328
+ if session.project_id:
329
+ session.project_name = project_map.get(session.project_id)
330
+
331
+ log.info(
332
+ "Search for '%s' by user %s returned %d results (total: %d)",
333
+ query,
334
+ user_id,
335
+ len(sessions),
336
+ total_count,
337
+ )
338
+
339
+ return PaginatedResponse.create(sessions, total_count, pagination)
340
+
169
341
  def save_task(
170
342
  self,
171
343
  db: DbSession,