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.
- solace_agent_mesh/agent/adk/alembic/README +74 -0
- solace_agent_mesh/agent/adk/alembic/env.py +77 -0
- solace_agent_mesh/agent/adk/alembic/script.py.mako +28 -0
- solace_agent_mesh/agent/adk/alembic/versions/e2902798564d_adk_session_db_upgrade.py +52 -0
- solace_agent_mesh/agent/adk/alembic.ini +112 -0
- solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
- solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +165 -1
- solace_agent_mesh/agent/adk/artifacts/s3_artifact_service.py +163 -0
- solace_agent_mesh/agent/adk/callbacks.py +852 -109
- solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +234 -36
- solace_agent_mesh/agent/adk/intelligent_mcp_callbacks.py +52 -5
- solace_agent_mesh/agent/adk/mcp_content_processor.py +1 -1
- solace_agent_mesh/agent/adk/models/lite_llm.py +77 -21
- solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +24 -137
- solace_agent_mesh/agent/adk/runner.py +85 -20
- solace_agent_mesh/agent/adk/schema_migration.py +88 -0
- solace_agent_mesh/agent/adk/services.py +94 -18
- solace_agent_mesh/agent/adk/setup.py +281 -65
- solace_agent_mesh/agent/adk/stream_parser.py +231 -37
- solace_agent_mesh/agent/adk/tool_wrapper.py +3 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +472 -137
- solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
- solace_agent_mesh/agent/proxies/a2a/component.py +572 -75
- solace_agent_mesh/agent/proxies/a2a/config.py +80 -4
- solace_agent_mesh/agent/proxies/base/app.py +3 -2
- solace_agent_mesh/agent/proxies/base/component.py +188 -22
- solace_agent_mesh/agent/proxies/base/proxy_task_context.py +3 -1
- solace_agent_mesh/agent/sac/app.py +91 -3
- solace_agent_mesh/agent/sac/component.py +591 -157
- solace_agent_mesh/agent/sac/patch_adk.py +8 -16
- solace_agent_mesh/agent/sac/task_execution_context.py +146 -4
- solace_agent_mesh/agent/tools/__init__.py +3 -0
- solace_agent_mesh/agent/tools/audio_tools.py +3 -3
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +710 -171
- solace_agent_mesh/agent/tools/deep_research_tools.py +2161 -0
- solace_agent_mesh/agent/tools/dynamic_tool.py +2 -0
- solace_agent_mesh/agent/tools/peer_agent_tool.py +82 -15
- solace_agent_mesh/agent/tools/time_tools.py +126 -0
- solace_agent_mesh/agent/tools/tool_config_types.py +57 -2
- solace_agent_mesh/agent/tools/web_search_tools.py +279 -0
- solace_agent_mesh/agent/tools/web_tools.py +125 -17
- solace_agent_mesh/agent/utils/artifact_helpers.py +248 -6
- solace_agent_mesh/agent/utils/context_helpers.py +17 -0
- solace_agent_mesh/assets/docs/404.html +6 -6
- solace_agent_mesh/assets/docs/assets/css/{styles.906a1503.css → styles.8162edfb.css} +1 -1
- solace_agent_mesh/assets/docs/assets/js/05749d90.19ac4f35.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.e186750d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/15e40e79.434bb30f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/17896441.e612dfb4.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2279.550aa580.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/{17896441.a5e82f9b.js.LICENSE.txt → 2279.550aa580.js.LICENSE.txt} +6 -0
- solace_agent_mesh/assets/docs/assets/js/240a0364.83e37aa8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2987107d.a80604f9.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.2f0db237.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3a6c6137.7e61915d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.7f7ab1c1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.e53c9b78.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/41adc471.0e95b87c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/4667dc50.bf2ad456.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/49eed117.493d6f99.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{509e993c.4c7a1a6d.js → 509e993c.a1fbf45a.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/547e15cc.8e6da617.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/55b7b518.29d6e75d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5b8d9c11.d4eb37b8.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.1ee87753.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/60702c0e.a8bdd79b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/631738c7.fa471607.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/64195356.09dbd087.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/66d4869e.30340bd3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.b6e3f2ce.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6aaedf65.7253541d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.a5b36a60.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.fd23ba4a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.374b9d54.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/729898df.7249e9fd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/7e294c01.7c5f6906.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8024126c.e3467286.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/81a99df0.7ed65d45.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.161823a5.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/8b032486.91a91afc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.975e428a.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/94e8668d.16083b3f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9bb13469.4523ae20.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a7d42657.a956689d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/a94703ab.3e5fbcb3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e563275.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ad87452a.9d73dad6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.0e0d8baf.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cab03b5b.6a073091.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/cbe2e9ea.07e170dd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.b62f7b08.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.c37a755e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.b682e9c2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de915948.44a432bc.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e04b235d.06d23db6.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e1b6eeb4.deb2b62e.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.1476f570.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.acc800d3.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/e92d0134.c147a429.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ee0c2fe7.94d0a351.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.cc97854c.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.74710fc1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/main.d634009f.js +2 -0
- solace_agent_mesh/assets/docs/assets/js/runtime~main.27bb82a7.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +68 -68
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +50 -50
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +42 -42
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +55 -55
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +82 -68
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/image-tools/index.html +81 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +67 -50
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/research-tools/index.html +136 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +178 -144
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +43 -42
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +20 -18
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +23 -23
- solace_agent_mesh/assets/docs/docs/documentation/components/platform-service/index.html +33 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +45 -45
- solace_agent_mesh/assets/docs/docs/documentation/components/projects/index.html +182 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/prompts/index.html +147 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +208 -125
- solace_agent_mesh/assets/docs/docs/documentation/components/speech/index.html +52 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +28 -49
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +29 -30
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/index.html +47 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/kubernetes/kubernetes-deployment-guide/index.html +197 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +90 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +17 -16
- solace_agent_mesh/assets/docs/docs/documentation/deploying/proxy_configuration/index.html +49 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +38 -38
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +162 -171
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +67 -49
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +17 -17
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +22 -22
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +27 -27
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +135 -135
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +66 -66
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +50 -38
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +86 -86
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +51 -51
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +24 -24
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +30 -30
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +44 -44
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/teams-integration/index.html +115 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/agent-builder/index.html +86 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/connectors/index.html +67 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +23 -19
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +40 -37
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/openapi-tools/index.html +324 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +112 -87
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/secure-user-delegated-access/index.html +440 -0
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +87 -64
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/wheel-installation/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +44 -44
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +39 -37
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +30 -30
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +18 -18
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/vibe_coding/index.html +62 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/artifact-storage/index.html +311 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +39 -42
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +14 -14
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +27 -25
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +69 -69
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +72 -72
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/session-storage/index.html +251 -0
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/user-feedback/index.html +88 -0
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +42 -42
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +20 -20
- solace_agent_mesh/assets/docs/docs/documentation/migrations/platform-service-split/index.html +85 -0
- solace_agent_mesh/assets/docs/lunr-index-1768329217460.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1768329217460.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/__init__.py +3 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +6 -1
- solace_agent_mesh/cli/commands/add_cmd/proxy_cmd.py +100 -0
- solace_agent_mesh/cli/commands/docs_cmd.py +4 -1
- solace_agent_mesh/cli/commands/eval_cmd.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +15 -0
- solace_agent_mesh/cli/commands/init_cmd/directory_step.py +1 -1
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +30 -3
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +3 -4
- solace_agent_mesh/cli/commands/init_cmd/platform_service_step.py +85 -0
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +16 -3
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +2 -1
- solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
- solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +3 -3
- solace_agent_mesh/cli/commands/run_cmd.py +64 -49
- solace_agent_mesh/cli/commands/tools_cmd.py +315 -0
- solace_agent_mesh/cli/main.py +15 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-KnKMP_vb.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/client-DpBL2stg.js +25 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-Cd498TV2.js +435 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-rSf8Vu29.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-CGk8Suyh.js +565 -0
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/client/webui/frontend/static/mockServiceWorker.js +336 -0
- solace_agent_mesh/client/webui/frontend/static/ui-version.json +6 -0
- solace_agent_mesh/common/a2a/events.py +2 -1
- solace_agent_mesh/common/a2a/protocol.py +5 -0
- solace_agent_mesh/common/a2a/types.py +2 -1
- solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +23 -6
- solace_agent_mesh/common/a2a_spec/schemas/feedback_event.json +51 -0
- solace_agent_mesh/common/agent_registry.py +38 -11
- solace_agent_mesh/common/data_parts.py +144 -4
- solace_agent_mesh/common/error_handlers.py +83 -0
- solace_agent_mesh/common/exceptions.py +24 -0
- solace_agent_mesh/common/oauth/__init__.py +17 -0
- solace_agent_mesh/common/oauth/oauth_client.py +408 -0
- solace_agent_mesh/common/oauth/utils.py +50 -0
- solace_agent_mesh/common/rag_dto.py +156 -0
- solace_agent_mesh/common/sac/sam_component_base.py +97 -19
- solace_agent_mesh/common/sam_events/event_service.py +2 -2
- solace_agent_mesh/common/services/employee_service.py +1 -1
- solace_agent_mesh/common/utils/embeds/constants.py +1 -0
- solace_agent_mesh/common/utils/embeds/converter.py +1 -8
- solace_agent_mesh/common/utils/embeds/modifiers.py +4 -28
- solace_agent_mesh/common/utils/embeds/resolver.py +152 -31
- solace_agent_mesh/common/utils/embeds/types.py +9 -0
- solace_agent_mesh/common/utils/log_formatters.py +20 -0
- solace_agent_mesh/common/utils/mime_helpers.py +12 -5
- solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
- solace_agent_mesh/common/utils/rbac_utils.py +69 -0
- solace_agent_mesh/common/utils/templates/__init__.py +8 -0
- solace_agent_mesh/common/utils/templates/liquid_renderer.py +210 -0
- solace_agent_mesh/common/utils/templates/template_resolver.py +161 -0
- solace_agent_mesh/config_portal/backend/common.py +12 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-CljP4_mv.js +103 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{components-Rk0n-9cK.js → components-CaC6hG8d.js} +22 -22
- solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-mvZjNKiz.js → entry.client-H_TM0YBt.js} +3 -3
- solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DzNKzXrc.js → index-CnFykb2v.js} +16 -16
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-f8439d40.js +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-BIMqslJB.css +1 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-mJmTIdIk.js +10 -0
- solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
- solace_agent_mesh/core_a2a/service.py +3 -2
- solace_agent_mesh/gateway/adapter/__init__.py +1 -0
- solace_agent_mesh/gateway/adapter/base.py +170 -0
- solace_agent_mesh/gateway/adapter/types.py +230 -0
- solace_agent_mesh/gateway/base/app.py +39 -2
- solace_agent_mesh/gateway/base/auth_interface.py +103 -0
- solace_agent_mesh/gateway/base/component.py +1027 -151
- solace_agent_mesh/gateway/generic/__init__.py +1 -0
- solace_agent_mesh/gateway/generic/app.py +50 -0
- solace_agent_mesh/gateway/generic/component.py +894 -0
- solace_agent_mesh/gateway/http_sse/alembic/env.py +0 -7
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_project_users_table.py +72 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251023_add_soft_delete_and_search.py +109 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_default_agent_to_projects.py +26 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251024_add_projects_table.py +135 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251108_create_prompt_tables_with_sharing.py +154 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251115_add_parent_task_id.py +32 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251126_add_background_task_fields.py +47 -0
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251202_add_versioned_fields_to_prompts.py +52 -0
- solace_agent_mesh/gateway/http_sse/alembic.ini +0 -36
- solace_agent_mesh/gateway/http_sse/app.py +40 -11
- solace_agent_mesh/gateway/http_sse/component.py +285 -160
- solace_agent_mesh/gateway/http_sse/dependencies.py +149 -114
- solace_agent_mesh/gateway/http_sse/main.py +68 -450
- solace_agent_mesh/gateway/http_sse/repository/__init__.py +19 -1
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +2 -2
- solace_agent_mesh/gateway/http_sse/repository/entities/project.py +81 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/project_user.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/entities/session.py +26 -3
- solace_agent_mesh/gateway/http_sse/repository/entities/task.py +7 -0
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +47 -0
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +114 -6
- solace_agent_mesh/gateway/http_sse/repository/models/__init__.py +13 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_model.py +51 -0
- solace_agent_mesh/gateway/http_sse/repository/models/project_user_model.py +75 -0
- solace_agent_mesh/gateway/http_sse/repository/models/prompt_model.py +159 -0
- solace_agent_mesh/gateway/http_sse/repository/models/session_model.py +8 -2
- solace_agent_mesh/gateway/http_sse/repository/models/task_model.py +8 -1
- solace_agent_mesh/gateway/http_sse/repository/project_repository.py +172 -0
- solace_agent_mesh/gateway/http_sse/repository/project_user_repository.py +186 -0
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +177 -11
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +86 -2
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +38 -7
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +256 -58
- solace_agent_mesh/gateway/http_sse/routers/auth.py +168 -134
- solace_agent_mesh/gateway/http_sse/routers/config.py +302 -8
- solace_agent_mesh/gateway/http_sse/routers/dto/project_dto.py +69 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/prompt_dto.py +255 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/project_requests.py +48 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/session_requests.py +14 -1
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/base_responses.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/project_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +5 -2
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/version_responses.py +31 -0
- solace_agent_mesh/gateway/http_sse/routers/feedback.py +133 -2
- solace_agent_mesh/gateway/http_sse/routers/people.py +2 -2
- solace_agent_mesh/gateway/http_sse/routers/projects.py +768 -0
- solace_agent_mesh/gateway/http_sse/routers/prompts.py +1416 -0
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +167 -7
- solace_agent_mesh/gateway/http_sse/routers/speech.py +355 -0
- solace_agent_mesh/gateway/http_sse/routers/sse.py +131 -8
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +670 -18
- solace_agent_mesh/gateway/http_sse/routers/users.py +1 -1
- solace_agent_mesh/gateway/http_sse/routers/version.py +343 -0
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +92 -9
- solace_agent_mesh/gateway/http_sse/services/audio_service.py +1227 -0
- solace_agent_mesh/gateway/http_sse/services/background_task_monitor.py +186 -0
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +1 -1
- solace_agent_mesh/gateway/http_sse/services/project_service.py +930 -0
- solace_agent_mesh/gateway/http_sse/services/prompt_builder_assistant.py +303 -0
- solace_agent_mesh/gateway/http_sse/services/session_service.py +361 -12
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +354 -4
- solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
- solace_agent_mesh/gateway/http_sse/sse_manager.py +286 -166
- solace_agent_mesh/gateway/http_sse/utils/artifact_copy_utils.py +370 -0
- solace_agent_mesh/gateway/http_sse/utils/stim_utils.py +41 -1
- solace_agent_mesh/services/__init__.py +0 -0
- solace_agent_mesh/services/platform/__init__.py +29 -0
- solace_agent_mesh/services/platform/alembic/env.py +85 -0
- solace_agent_mesh/services/platform/alembic/script.py.mako +28 -0
- solace_agent_mesh/services/platform/alembic.ini +109 -0
- solace_agent_mesh/services/platform/api/__init__.py +3 -0
- solace_agent_mesh/services/platform/api/dependencies.py +154 -0
- solace_agent_mesh/services/platform/api/main.py +314 -0
- solace_agent_mesh/services/platform/api/middleware.py +51 -0
- solace_agent_mesh/services/platform/api/routers/__init__.py +33 -0
- solace_agent_mesh/services/platform/api/routers/health_router.py +31 -0
- solace_agent_mesh/services/platform/app.py +215 -0
- solace_agent_mesh/services/platform/component.py +777 -0
- solace_agent_mesh/shared/__init__.py +14 -0
- solace_agent_mesh/shared/api/__init__.py +42 -0
- solace_agent_mesh/shared/auth/__init__.py +26 -0
- solace_agent_mesh/shared/auth/dependencies.py +204 -0
- solace_agent_mesh/shared/auth/middleware.py +347 -0
- solace_agent_mesh/shared/database/__init__.py +20 -0
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/base_repository.py +1 -1
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_exceptions.py +1 -1
- solace_agent_mesh/{gateway/http_sse/shared → shared/database}/database_helpers.py +1 -1
- solace_agent_mesh/shared/exceptions/__init__.py +36 -0
- solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exception_handlers.py +19 -5
- solace_agent_mesh/shared/utils/__init__.py +21 -0
- solace_agent_mesh/templates/logging_config_template.yaml +48 -0
- solace_agent_mesh/templates/main_orchestrator.yaml +12 -1
- solace_agent_mesh/templates/platform.yaml +49 -0
- solace_agent_mesh/templates/plugin_readme_template.md +3 -25
- solace_agent_mesh/templates/plugin_tool_config_template.yaml +109 -0
- solace_agent_mesh/templates/proxy_template.yaml +62 -0
- solace_agent_mesh/templates/webui.yaml +148 -6
- solace_agent_mesh/tools/web_search/__init__.py +18 -0
- solace_agent_mesh/tools/web_search/base.py +84 -0
- solace_agent_mesh/tools/web_search/google_search.py +247 -0
- solace_agent_mesh/tools/web_search/models.py +99 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/METADATA +31 -12
- solace_agent_mesh-1.13.2.dist-info/RECORD +591 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/WHEEL +1 -1
- solace_agent_mesh/agent/adk/adk_llm.txt +0 -232
- solace_agent_mesh/agent/adk/adk_llm_detail.txt +0 -566
- solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +0 -171
- solace_agent_mesh/agent/adk/models/models_llm.txt +0 -142
- solace_agent_mesh/agent/agent_llm.txt +0 -378
- solace_agent_mesh/agent/agent_llm_detail.txt +0 -1702
- solace_agent_mesh/agent/protocol/protocol_llm.txt +0 -81
- solace_agent_mesh/agent/protocol/protocol_llm_detail.txt +0 -92
- solace_agent_mesh/agent/sac/sac_llm.txt +0 -189
- solace_agent_mesh/agent/sac/sac_llm_detail.txt +0 -200
- solace_agent_mesh/agent/testing/testing_llm.txt +0 -57
- solace_agent_mesh/agent/testing/testing_llm_detail.txt +0 -68
- solace_agent_mesh/agent/tools/tools_llm.txt +0 -263
- solace_agent_mesh/agent/tools/tools_llm_detail.txt +0 -274
- solace_agent_mesh/agent/utils/utils_llm.txt +0 -138
- solace_agent_mesh/agent/utils/utils_llm_detail.txt +0 -149
- solace_agent_mesh/assets/docs/assets/js/15ba94aa.932dd2db.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/2e32b5e0.33f5d75b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3a6c6137.f5940cfa.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3ac1795d.76654dd9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/3ff0015d.2be20244.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/547e15cc.2cbb060a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/55b7b518.f2b1d1ba.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6a520c9d.ba015d81.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6d84eae0.4a5fbf39.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/8024126c.56e59919.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/81a99df0.07034dd9.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/82fbfb93.139a1a1f.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/94e8668d.b5ddb7a1.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/9bb13469.dd1c9b54.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ab9708a8.3e6dd091.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/c93cbaa0.eaff365e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/da0b5bad.d08a9466.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/dd817ffc.0aa9630a.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/dd81e2b8.d590bc9e.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e3d9abda.6b9493d0.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/main.b12eac43.js +0 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1761248203150.json +0 -1
- solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +0 -250
- solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +0 -365
- solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +0 -305
- solace_agent_mesh/client/webui/frontend/static/assets/client-CaY59VuC.js +0 -25
- solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +0 -342
- solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/vendor-BEmvJSYz.js +0 -405
- solace_agent_mesh/common/a2a/a2a_llm.txt +0 -182
- solace_agent_mesh/common/a2a/a2a_llm_detail.txt +0 -193
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +0 -407
- solace_agent_mesh/common/a2a_spec/a2a_spec_llm_detail.txt +0 -736
- solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +0 -313
- solace_agent_mesh/common/common_llm.txt +0 -251
- solace_agent_mesh/common/common_llm_detail.txt +0 -2562
- solace_agent_mesh/common/middleware/middleware_llm.txt +0 -174
- solace_agent_mesh/common/middleware/middleware_llm_detail.txt +0 -185
- solace_agent_mesh/common/sac/sac_llm.txt +0 -71
- solace_agent_mesh/common/sac/sac_llm_detail.txt +0 -82
- solace_agent_mesh/common/sam_events/sam_events_llm.txt +0 -104
- solace_agent_mesh/common/sam_events/sam_events_llm_detail.txt +0 -115
- solace_agent_mesh/common/services/providers/providers_llm.txt +0 -80
- solace_agent_mesh/common/services/services_llm.txt +0 -363
- solace_agent_mesh/common/services/services_llm_detail.txt +0 -459
- solace_agent_mesh/common/utils/embeds/embeds_llm.txt +0 -220
- solace_agent_mesh/common/utils/utils_llm.txt +0 -336
- solace_agent_mesh/common/utils/utils_llm_detail.txt +0 -572
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +0 -98
- solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-61038fc6.js +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-BWvk5-gF.js +0 -10
- solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +0 -1
- solace_agent_mesh/core_a2a/core_a2a_llm.txt +0 -90
- solace_agent_mesh/core_a2a/core_a2a_llm_detail.txt +0 -101
- solace_agent_mesh/gateway/base/base_llm.txt +0 -224
- solace_agent_mesh/gateway/base/base_llm_detail.txt +0 -235
- solace_agent_mesh/gateway/gateway_llm.txt +0 -373
- solace_agent_mesh/gateway/gateway_llm_detail.txt +0 -3885
- solace_agent_mesh/gateway/http_sse/alembic/alembic_llm.txt +0 -295
- solace_agent_mesh/gateway/http_sse/alembic/versions/versions_llm.txt +0 -155
- solace_agent_mesh/gateway/http_sse/components/components_llm.txt +0 -105
- solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +0 -299
- solace_agent_mesh/gateway/http_sse/http_sse_llm_detail.txt +0 -3278
- solace_agent_mesh/gateway/http_sse/repository/entities/entities_llm.txt +0 -263
- solace_agent_mesh/gateway/http_sse/repository/models/models_llm.txt +0 -266
- solace_agent_mesh/gateway/http_sse/repository/repository_llm.txt +0 -340
- solace_agent_mesh/gateway/http_sse/routers/dto/dto_llm.txt +0 -346
- solace_agent_mesh/gateway/http_sse/routers/dto/requests/requests_llm.txt +0 -83
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/responses_llm.txt +0 -107
- solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +0 -314
- solace_agent_mesh/gateway/http_sse/services/services_llm.txt +0 -297
- solace_agent_mesh/gateway/http_sse/shared/__init__.py +0 -146
- solace_agent_mesh/gateway/http_sse/shared/shared_llm.txt +0 -285
- solace_agent_mesh/gateway/http_sse/utils/utils_llm.txt +0 -47
- solace_agent_mesh/llm.txt +0 -228
- solace_agent_mesh/llm_detail.txt +0 -2835
- solace_agent_mesh/solace_agent_mesh_llm.txt +0 -362
- solace_agent_mesh/solace_agent_mesh_llm_detail.txt +0 -8599
- solace_agent_mesh/templates/logging_config_template.ini +0 -45
- solace_agent_mesh/templates/templates_llm.txt +0 -147
- solace_agent_mesh-1.6.1.dist-info/RECORD +0 -525
- /solace_agent_mesh/assets/docs/assets/js/{main.b12eac43.js.LICENSE.txt → main.d634009f.js.LICENSE.txt} +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/auth_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/pagination.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/api}/response_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/error_dto.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/exceptions}/exceptions.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/enums.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/timestamp_utils.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/types.py +0 -0
- /solace_agent_mesh/{gateway/http_sse/shared → shared/utils}/utils.py +0 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.6.1.dist-info → solace_agent_mesh-1.13.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,2161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deep Research Tools for Solace Agent Mesh
|
|
3
|
+
|
|
4
|
+
Provides comprehensive, iterative research capabilities using web search
|
|
5
|
+
|
|
6
|
+
This module implements:
|
|
7
|
+
- Iterative research with LLM-powered reflection and query refinement
|
|
8
|
+
- Multi-source search coordination
|
|
9
|
+
- Citation tracking and management
|
|
10
|
+
- Progress updates to frontend
|
|
11
|
+
- Comprehensive report generation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import uuid
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from google.adk.tools import ToolContext
|
|
23
|
+
from google.genai import types as adk_types
|
|
24
|
+
from google.adk.models import LlmRequest
|
|
25
|
+
from solace_ai_connector.common.log import log
|
|
26
|
+
|
|
27
|
+
from .tool_definition import BuiltinTool
|
|
28
|
+
from .registry import tool_registry
|
|
29
|
+
from .web_search_tools import web_search_google
|
|
30
|
+
from .web_tools import web_request
|
|
31
|
+
from ...common import a2a
|
|
32
|
+
from ...common.rag_dto import create_rag_source, create_rag_search_result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Category information
|
|
36
|
+
CATEGORY_NAME = "Research & Analysis"
|
|
37
|
+
CATEGORY_DESCRIPTION = "Advanced research tools for comprehensive information gathering"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _extract_text_from_llm_response(response: Any, log_identifier: str = "[LLM]") -> str:
|
|
41
|
+
"""
|
|
42
|
+
Extract text from various LLM response formats.
|
|
43
|
+
|
|
44
|
+
Handles multiple response structures:
|
|
45
|
+
- Direct text attribute (response.text)
|
|
46
|
+
- Parts attribute for streaming responses (response.parts)
|
|
47
|
+
- Content attribute with parts for LlmResponse objects (response.content.parts)
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
response: The LLM response object
|
|
51
|
+
log_identifier: Identifier for logging
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Extracted text string, or empty string if extraction fails
|
|
55
|
+
"""
|
|
56
|
+
response_text = ""
|
|
57
|
+
|
|
58
|
+
# Method 1: Direct text attribute
|
|
59
|
+
if hasattr(response, 'text') and response.text:
|
|
60
|
+
response_text = response.text
|
|
61
|
+
# Method 2: Parts attribute (for streaming responses)
|
|
62
|
+
elif hasattr(response, 'parts') and response.parts:
|
|
63
|
+
response_text = "".join([part.text for part in response.parts if hasattr(part, 'text') and part.text])
|
|
64
|
+
# Method 3: Content attribute with parts (for LlmResponse objects from Gemini 2.5 Pro)
|
|
65
|
+
elif hasattr(response, 'content') and response.content:
|
|
66
|
+
content = response.content
|
|
67
|
+
if hasattr(content, 'parts') and content.parts:
|
|
68
|
+
response_text = "".join([part.text for part in content.parts if hasattr(part, 'text') and part.text])
|
|
69
|
+
elif hasattr(content, 'text') and content.text:
|
|
70
|
+
response_text = content.text
|
|
71
|
+
elif isinstance(content, str):
|
|
72
|
+
response_text = content
|
|
73
|
+
|
|
74
|
+
if not response_text or not response_text.strip():
|
|
75
|
+
log.warning("%s Could not extract text from LLM response. Response type: %s",
|
|
76
|
+
log_identifier, type(response).__name__)
|
|
77
|
+
if response:
|
|
78
|
+
log.debug("%s Response attributes: text=%s, parts=%s, content=%s",
|
|
79
|
+
log_identifier,
|
|
80
|
+
hasattr(response, 'text'),
|
|
81
|
+
hasattr(response, 'parts'),
|
|
82
|
+
hasattr(response, 'content'))
|
|
83
|
+
|
|
84
|
+
return response_text
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_json_from_llm_response(
|
|
88
|
+
response_text: str,
|
|
89
|
+
log_identifier: str = "[LLM]",
|
|
90
|
+
fallback_key: Optional[str] = None
|
|
91
|
+
) -> Optional[Dict[str, Any]]:
|
|
92
|
+
"""
|
|
93
|
+
Parse JSON from LLM response text, handling markdown code blocks.
|
|
94
|
+
|
|
95
|
+
Gemini 2.5 Pro and other models often wrap JSON in markdown code blocks
|
|
96
|
+
(```json ... ```) even when response_mime_type="application/json" is set.
|
|
97
|
+
This function handles that case.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
response_text: The raw response text from the LLM
|
|
101
|
+
log_identifier: Identifier for logging
|
|
102
|
+
fallback_key: Optional key to search for in regex fallback (e.g., "queries", "selected_sources")
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Parsed JSON dict, or None if parsing fails
|
|
106
|
+
"""
|
|
107
|
+
if not response_text or not response_text.strip():
|
|
108
|
+
log.warning("%s Empty response text, cannot parse JSON", log_identifier)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Strip markdown code block wrapper if present (common with Gemini 2.5 Pro)
|
|
112
|
+
clean_text = response_text.strip()
|
|
113
|
+
if clean_text.startswith('```'):
|
|
114
|
+
# Remove opening ```json or ``` and closing ```
|
|
115
|
+
if clean_text.startswith('```json'):
|
|
116
|
+
clean_text = clean_text[7:] # len('```json') = 7
|
|
117
|
+
else:
|
|
118
|
+
clean_text = clean_text[3:] # len('```') = 3
|
|
119
|
+
clean_text = clean_text.lstrip()
|
|
120
|
+
|
|
121
|
+
# Remove closing ``` and trailing whitespace
|
|
122
|
+
clean_text = clean_text.rstrip()
|
|
123
|
+
if clean_text.endswith('```'):
|
|
124
|
+
clean_text = clean_text[:-3] # Remove trailing ```
|
|
125
|
+
clean_text = clean_text.rstrip() # Remove any whitespace before closing ```
|
|
126
|
+
log.debug("%s Stripped markdown code block wrapper", log_identifier)
|
|
127
|
+
|
|
128
|
+
# Try to parse JSON directly
|
|
129
|
+
try:
|
|
130
|
+
return json.loads(clean_text)
|
|
131
|
+
except json.JSONDecodeError as je:
|
|
132
|
+
log.warning("%s Failed to parse LLM JSON response: %s. Response text: %s",
|
|
133
|
+
log_identifier, str(je), clean_text[:200])
|
|
134
|
+
|
|
135
|
+
# Fallback: Try to extract JSON from markdown code blocks (in case stripping didn't work)
|
|
136
|
+
json_match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', response_text, re.DOTALL)
|
|
137
|
+
if json_match:
|
|
138
|
+
try:
|
|
139
|
+
result = json.loads(json_match.group(1))
|
|
140
|
+
log.info("%s Extracted JSON from markdown code block", log_identifier)
|
|
141
|
+
return result
|
|
142
|
+
except json.JSONDecodeError:
|
|
143
|
+
log.warning("%s Failed to parse extracted JSON from code block", log_identifier)
|
|
144
|
+
|
|
145
|
+
# Fallback: Try to find any JSON object with the specified key
|
|
146
|
+
if fallback_key:
|
|
147
|
+
# Build a regex pattern to find JSON with the specified key
|
|
148
|
+
json_match = re.search(rf'\{{[^{{}}]*"{fallback_key}"[^{{}}]*\}}', response_text, re.DOTALL)
|
|
149
|
+
if json_match:
|
|
150
|
+
try:
|
|
151
|
+
result = json.loads(json_match.group(0))
|
|
152
|
+
log.info("%s Extracted JSON object with key '%s' from response", log_identifier, fallback_key)
|
|
153
|
+
return result
|
|
154
|
+
except json.JSONDecodeError:
|
|
155
|
+
log.warning("%s Failed to parse extracted JSON object with key '%s'", log_identifier, fallback_key)
|
|
156
|
+
|
|
157
|
+
log.warning("%s No valid JSON found in response", log_identifier)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class SearchResult:
|
|
163
|
+
"""Represents a single search result from any source (web-only version)"""
|
|
164
|
+
source_type: str # "web" only, for now
|
|
165
|
+
title: str
|
|
166
|
+
content: str
|
|
167
|
+
url: Optional[str] = None
|
|
168
|
+
relevance_score: float = 0.0
|
|
169
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
170
|
+
citation_id: Optional[str] = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class ReflectionResult:
|
|
175
|
+
"""Result of reflecting on current research findings"""
|
|
176
|
+
quality_score: float # 0-1 score of information completeness
|
|
177
|
+
gaps: List[str] # Identified knowledge gaps
|
|
178
|
+
should_continue: bool # Whether more research is needed
|
|
179
|
+
suggested_queries: List[str] # New queries to explore gaps
|
|
180
|
+
reasoning: str # Explanation of the reflection
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _get_model_for_phase(
|
|
184
|
+
phase: str,
|
|
185
|
+
tool_context: ToolContext,
|
|
186
|
+
tool_config: Optional[Dict[str, Any]]
|
|
187
|
+
):
|
|
188
|
+
"""
|
|
189
|
+
Get the appropriate model for a specific research phase.
|
|
190
|
+
|
|
191
|
+
Supports phase-specific model configuration for cost optimization,
|
|
192
|
+
speed tuning, and quality control.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
phase: One of 'query_generation', 'reflection', 'source_selection', 'report_generation'
|
|
196
|
+
tool_context: Tool context for accessing agent
|
|
197
|
+
tool_config: Tool configuration with optional phase-specific models
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
BaseLlm instance for the phase (either phase-specific or agent default)
|
|
201
|
+
|
|
202
|
+
Configuration Examples:
|
|
203
|
+
# Simple model names:
|
|
204
|
+
tool_config:
|
|
205
|
+
models:
|
|
206
|
+
query_generation: "gpt-4o-mini"
|
|
207
|
+
report_generation: "claude-3-5-sonnet-20241022"
|
|
208
|
+
|
|
209
|
+
# Full model configs with parameters:
|
|
210
|
+
tool_config:
|
|
211
|
+
model_configs:
|
|
212
|
+
report_generation:
|
|
213
|
+
model: "claude-3-5-sonnet-20241022"
|
|
214
|
+
temperature: 0.7
|
|
215
|
+
max_tokens: 16000
|
|
216
|
+
"""
|
|
217
|
+
log_identifier = f"[DeepResearch:ModelSelection:{phase}]"
|
|
218
|
+
|
|
219
|
+
# Get agent's default model
|
|
220
|
+
inv_context = tool_context._invocation_context
|
|
221
|
+
agent = getattr(inv_context, 'agent', None)
|
|
222
|
+
default_model = agent.canonical_model if agent else None
|
|
223
|
+
|
|
224
|
+
# If canonical_model is not available, try to get model from host_component config
|
|
225
|
+
if not default_model:
|
|
226
|
+
host_component = getattr(agent, "host_component", None) if agent else None
|
|
227
|
+
if host_component:
|
|
228
|
+
model_config_from_component = host_component.get_config("model")
|
|
229
|
+
if model_config_from_component:
|
|
230
|
+
log.info(
|
|
231
|
+
"%s canonical_model not available, falling back to host_component model config",
|
|
232
|
+
log_identifier,
|
|
233
|
+
)
|
|
234
|
+
from ...agent.adk.models.lite_llm import LiteLlm
|
|
235
|
+
if isinstance(model_config_from_component, str):
|
|
236
|
+
default_model = LiteLlm(model=model_config_from_component)
|
|
237
|
+
elif isinstance(model_config_from_component, dict):
|
|
238
|
+
default_model = LiteLlm(**model_config_from_component)
|
|
239
|
+
|
|
240
|
+
if not default_model:
|
|
241
|
+
raise ValueError(f"{log_identifier} No default model available")
|
|
242
|
+
|
|
243
|
+
# Check for phase-specific configuration
|
|
244
|
+
if not tool_config:
|
|
245
|
+
log.debug("%s No tool_config, using agent default model", log_identifier)
|
|
246
|
+
return default_model
|
|
247
|
+
|
|
248
|
+
# Helper function to copy base config from default model
|
|
249
|
+
def _get_base_config_from_default():
|
|
250
|
+
"""Extract base configuration from default model to inherit API keys and settings"""
|
|
251
|
+
base_config = {}
|
|
252
|
+
if hasattr(default_model, '_additional_args') and default_model._additional_args:
|
|
253
|
+
# Copy relevant config from default model (API keys, timeouts, custom endpoints, etc.)
|
|
254
|
+
# Exclude model-specific params that shouldn't be inherited
|
|
255
|
+
# Also exclude max_completion_tokens to avoid conflicts with max_tokens
|
|
256
|
+
exclude_keys = {'model', 'messages', 'tools', 'stream', 'temperature', 'max_tokens',
|
|
257
|
+
'max_output_tokens', 'max_completion_tokens', 'top_p', 'top_k'}
|
|
258
|
+
base_config = {k: v for k, v in default_model._additional_args.items()
|
|
259
|
+
if k not in exclude_keys}
|
|
260
|
+
|
|
261
|
+
# Log inherited configuration for debugging
|
|
262
|
+
if base_config:
|
|
263
|
+
log.debug("%s Inheriting base config from default model: api_base=%s, api_key=%s",
|
|
264
|
+
log_identifier,
|
|
265
|
+
base_config.get('api_base', 'default'),
|
|
266
|
+
'present' if base_config.get('api_key') else 'missing')
|
|
267
|
+
return base_config
|
|
268
|
+
|
|
269
|
+
# Option 1: Simple model name string
|
|
270
|
+
models_config = tool_config.get("models", {})
|
|
271
|
+
if phase in models_config:
|
|
272
|
+
model_name = models_config[phase]
|
|
273
|
+
if isinstance(model_name, str):
|
|
274
|
+
log.info("%s Using phase-specific model: %s", log_identifier, model_name)
|
|
275
|
+
from ...agent.adk.models.lite_llm import LiteLlm
|
|
276
|
+
# Inherit base config from default model (API keys, etc.)
|
|
277
|
+
base_config = _get_base_config_from_default()
|
|
278
|
+
return LiteLlm(model=model_name, **base_config)
|
|
279
|
+
|
|
280
|
+
# Option 2: Full model configuration dict
|
|
281
|
+
model_configs = tool_config.get("model_configs", {})
|
|
282
|
+
if phase in model_configs:
|
|
283
|
+
model_config = model_configs[phase]
|
|
284
|
+
if isinstance(model_config, dict):
|
|
285
|
+
model_name = model_config.get("model")
|
|
286
|
+
log.info("%s Using phase-specific model config: %s (temp=%.1f, max_tokens=%s)",
|
|
287
|
+
log_identifier, model_name,
|
|
288
|
+
model_config.get("temperature", 0.7),
|
|
289
|
+
model_config.get("max_tokens", "default"))
|
|
290
|
+
from ...agent.adk.models.lite_llm import LiteLlm
|
|
291
|
+
# Inherit base config from default model, but allow override
|
|
292
|
+
base_config = _get_base_config_from_default()
|
|
293
|
+
# Merge: base_config first, then model_config (model_config takes precedence)
|
|
294
|
+
merged_config = {**base_config, **model_config}
|
|
295
|
+
|
|
296
|
+
# Additional safety: if max_tokens is specified, ensure max_completion_tokens is not present
|
|
297
|
+
if 'max_tokens' in merged_config and 'max_completion_tokens' in merged_config:
|
|
298
|
+
log.debug("%s Removing max_completion_tokens to avoid conflict with max_tokens", log_identifier)
|
|
299
|
+
del merged_config['max_completion_tokens']
|
|
300
|
+
|
|
301
|
+
return LiteLlm(**merged_config)
|
|
302
|
+
|
|
303
|
+
# Fallback to agent default
|
|
304
|
+
log.debug("%s No phase-specific model configured, using agent default", log_identifier)
|
|
305
|
+
return default_model
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ResearchCitationTracker:
|
|
309
|
+
"""Tracks citations throughout the research process"""
|
|
310
|
+
|
|
311
|
+
def __init__(self, research_question: str):
|
|
312
|
+
self.research_question = research_question
|
|
313
|
+
self.citations: Dict[str, Dict[str, Any]] = {}
|
|
314
|
+
self.citation_counter = 0
|
|
315
|
+
self.source_to_citation: Dict[str, str] = {} # Map URL to citation_id for updates
|
|
316
|
+
self.queries: List[Dict[str, Any]] = [] # Track queries and their sources
|
|
317
|
+
self.current_query: Optional[str] = None
|
|
318
|
+
self.current_query_sources: List[str] = []
|
|
319
|
+
self.generated_title: Optional[str] = None # LLM-generated human-readable title
|
|
320
|
+
|
|
321
|
+
def set_title(self, title: str) -> None:
|
|
322
|
+
"""Set the LLM-generated title for this research"""
|
|
323
|
+
self.generated_title = title
|
|
324
|
+
|
|
325
|
+
def start_query(self, query: str):
|
|
326
|
+
"""Start tracking a new query"""
|
|
327
|
+
# Save previous query if it exists
|
|
328
|
+
if self.current_query:
|
|
329
|
+
self.queries.append({
|
|
330
|
+
"query": self.current_query,
|
|
331
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
332
|
+
"source_citation_ids": self.current_query_sources.copy()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
# Start new query
|
|
336
|
+
self.current_query = query
|
|
337
|
+
self.current_query_sources = []
|
|
338
|
+
|
|
339
|
+
def add_citation(self, result: SearchResult, query: Optional[str] = None) -> str:
|
|
340
|
+
"""Add citation and return citation ID"""
|
|
341
|
+
# Use 'search' prefix to match the citation rendering system
|
|
342
|
+
citation_id = f"search{self.citation_counter}"
|
|
343
|
+
log.info("[DeepResearch:Citation] Creating citation_id=%s (counter=%d) for: %s",
|
|
344
|
+
citation_id, self.citation_counter, result.title[:50])
|
|
345
|
+
self.citation_counter += 1
|
|
346
|
+
|
|
347
|
+
# Create citation using DTO helper for camelCase conversion
|
|
348
|
+
citation_dict = create_rag_source(
|
|
349
|
+
citation_id=citation_id,
|
|
350
|
+
file_id=f"deep_research_{self.citation_counter}",
|
|
351
|
+
filename=result.title,
|
|
352
|
+
title=result.title,
|
|
353
|
+
source_url=result.url or "N/A",
|
|
354
|
+
url=result.url,
|
|
355
|
+
content_preview=result.content[:200] + "..." if len(result.content) > 200 else result.content,
|
|
356
|
+
relevance_score=result.relevance_score,
|
|
357
|
+
source_type=result.source_type,
|
|
358
|
+
retrieved_at=datetime.now(timezone.utc).isoformat(),
|
|
359
|
+
metadata={
|
|
360
|
+
"title": result.title,
|
|
361
|
+
"link": result.url,
|
|
362
|
+
"type": "web_search",
|
|
363
|
+
"source_type": result.source_type,
|
|
364
|
+
"favicon": f"https://www.google.com/s2/favicons?domain={result.url}&sz=32" if result.url else "",
|
|
365
|
+
**result.metadata
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
self.citations[citation_id] = citation_dict
|
|
370
|
+
result.citation_id = citation_id
|
|
371
|
+
|
|
372
|
+
# Track URL to citation_id mapping for later updates
|
|
373
|
+
if result.url:
|
|
374
|
+
self.source_to_citation[result.url] = citation_id
|
|
375
|
+
|
|
376
|
+
# Track this citation for the current query
|
|
377
|
+
if self.current_query:
|
|
378
|
+
self.current_query_sources.append(citation_id)
|
|
379
|
+
|
|
380
|
+
return citation_id
|
|
381
|
+
|
|
382
|
+
def update_citation_after_fetch(self, result: SearchResult) -> None:
|
|
383
|
+
"""Update citation with fetched content and metadata"""
|
|
384
|
+
if not result.url or result.url not in self.source_to_citation:
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
citation_id = self.source_to_citation[result.url]
|
|
388
|
+
if citation_id in self.citations:
|
|
389
|
+
# Update content preview with fetched content
|
|
390
|
+
self.citations[citation_id]["content_preview"] = result.content[:500] + "..." if len(result.content) > 500 else result.content
|
|
391
|
+
# Update metadata with fetched flag
|
|
392
|
+
self.citations[citation_id]["metadata"]["fetched"] = result.metadata.get("fetched", False)
|
|
393
|
+
self.citations[citation_id]["metadata"]["fetch_status"] = result.metadata.get("fetch_status", "")
|
|
394
|
+
log.info("[DeepResearch:Citation] Updated citation %s with fetched content", citation_id)
|
|
395
|
+
|
|
396
|
+
def get_rag_metadata(self, artifact_filename: Optional[str] = None) -> Dict[str, Any]:
|
|
397
|
+
"""Format citations for RAG system with camelCase conversion"""
|
|
398
|
+
# Save the last query if it exists
|
|
399
|
+
if self.current_query:
|
|
400
|
+
self.queries.append({
|
|
401
|
+
"query": self.current_query,
|
|
402
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
403
|
+
"source_citation_ids": self.current_query_sources.copy()
|
|
404
|
+
})
|
|
405
|
+
self.current_query = None
|
|
406
|
+
self.current_query_sources = []
|
|
407
|
+
|
|
408
|
+
# Build metadata dict
|
|
409
|
+
metadata_dict: Dict[str, Any] = {
|
|
410
|
+
"queries": self.queries # Include query breakdown for timeline
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# Include artifact filename if provided (for matching after page refresh)
|
|
414
|
+
if artifact_filename:
|
|
415
|
+
metadata_dict["artifactFilename"] = artifact_filename
|
|
416
|
+
|
|
417
|
+
# Return single search result with all sources using DTO for camelCase conversion
|
|
418
|
+
return create_rag_search_result(
|
|
419
|
+
query=self.research_question,
|
|
420
|
+
search_type="deep_research",
|
|
421
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
422
|
+
sources=list(self.citations.values()),
|
|
423
|
+
metadata=metadata_dict,
|
|
424
|
+
title=self.generated_title
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
async def _send_research_progress(
|
|
429
|
+
message: str,
|
|
430
|
+
tool_context: ToolContext,
|
|
431
|
+
phase: str = "",
|
|
432
|
+
progress_percentage: int = 0,
|
|
433
|
+
current_iteration: int = 0,
|
|
434
|
+
total_iterations: int = 0,
|
|
435
|
+
sources_found: int = 0,
|
|
436
|
+
current_query: str = "",
|
|
437
|
+
fetching_urls: Optional[List[Dict[str, str]]] = None,
|
|
438
|
+
elapsed_seconds: int = 0,
|
|
439
|
+
max_runtime_seconds: int = 0
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Send research progress update to frontend via SSE with structured data"""
|
|
442
|
+
log_identifier = "[DeepResearch:Progress]"
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
# Get a2a context from tool context state
|
|
446
|
+
a2a_context = tool_context.state.get("a2a_context")
|
|
447
|
+
if not a2a_context:
|
|
448
|
+
log.warning("%s No a2a_context found, cannot send progress update", log_identifier)
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
# Get the host component from invocation context
|
|
452
|
+
invocation_context = getattr(tool_context, '_invocation_context', None)
|
|
453
|
+
if not invocation_context:
|
|
454
|
+
log.warning("%s No invocation context found", log_identifier)
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
agent = getattr(invocation_context, 'agent', None)
|
|
458
|
+
if not agent:
|
|
459
|
+
log.warning("%s No agent found in invocation context", log_identifier)
|
|
460
|
+
return
|
|
461
|
+
|
|
462
|
+
host_component = getattr(agent, 'host_component', None)
|
|
463
|
+
if not host_component:
|
|
464
|
+
log.warning("%s No host component found on agent", log_identifier)
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
log.info("%s Sending progress: %s", log_identifier, message)
|
|
468
|
+
|
|
469
|
+
# Use structured DeepResearchProgressData if phase is provided, otherwise simple text
|
|
470
|
+
from ...common.data_parts import DeepResearchProgressData, AgentProgressUpdateData
|
|
471
|
+
|
|
472
|
+
if phase:
|
|
473
|
+
# Send structured progress data for UI visualization
|
|
474
|
+
progress_data = DeepResearchProgressData(
|
|
475
|
+
phase=phase,
|
|
476
|
+
status_text=message,
|
|
477
|
+
progress_percentage=progress_percentage,
|
|
478
|
+
current_iteration=current_iteration,
|
|
479
|
+
total_iterations=total_iterations,
|
|
480
|
+
sources_found=sources_found,
|
|
481
|
+
current_query=current_query,
|
|
482
|
+
fetching_urls=fetching_urls or [],
|
|
483
|
+
elapsed_seconds=elapsed_seconds,
|
|
484
|
+
max_runtime_seconds=max_runtime_seconds
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
# Fallback to simple text progress
|
|
488
|
+
progress_data = AgentProgressUpdateData(status_text=message)
|
|
489
|
+
|
|
490
|
+
# Use the host component's helper method to publish the data signal
|
|
491
|
+
host_component.publish_data_signal_from_thread(
|
|
492
|
+
a2a_context=a2a_context,
|
|
493
|
+
signal_data=progress_data,
|
|
494
|
+
skip_buffer_flush=False,
|
|
495
|
+
log_identifier=log_identifier,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
log.error("%s Error sending progress update: %s", log_identifier, str(e))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
async def _send_rag_info_update(
|
|
503
|
+
citation_tracker: 'ResearchCitationTracker',
|
|
504
|
+
tool_context: ToolContext,
|
|
505
|
+
is_complete: bool = False
|
|
506
|
+
) -> None:
|
|
507
|
+
"""
|
|
508
|
+
Send RAG info update to frontend via SSE for the RAG info panel.
|
|
509
|
+
|
|
510
|
+
This sends the title and sources early so the UI can display them
|
|
511
|
+
while research is still in progress.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
citation_tracker: The citation tracker with title and sources
|
|
515
|
+
tool_context: Tool context for accessing agent
|
|
516
|
+
is_complete: Whether the research is complete
|
|
517
|
+
"""
|
|
518
|
+
log_identifier = "[DeepResearch:RAGInfo]"
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
# Get a2a context from tool context state
|
|
522
|
+
a2a_context = tool_context.state.get("a2a_context")
|
|
523
|
+
if not a2a_context:
|
|
524
|
+
log.warning("%s No a2a_context found, cannot send RAG info update", log_identifier)
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
# Get the host component from invocation context
|
|
528
|
+
invocation_context = getattr(tool_context, '_invocation_context', None)
|
|
529
|
+
if not invocation_context:
|
|
530
|
+
log.warning("%s No invocation context found", log_identifier)
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
agent = getattr(invocation_context, 'agent', None)
|
|
534
|
+
if not agent:
|
|
535
|
+
log.warning("%s No agent found in invocation context", log_identifier)
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
host_component = getattr(agent, 'host_component', None)
|
|
539
|
+
if not host_component:
|
|
540
|
+
log.warning("%s No host component found on agent", log_identifier)
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
# Get title (use research question as fallback)
|
|
544
|
+
title = citation_tracker.generated_title or citation_tracker.research_question
|
|
545
|
+
|
|
546
|
+
# Get sources in camelCase format for frontend
|
|
547
|
+
sources = list(citation_tracker.citations.values())
|
|
548
|
+
|
|
549
|
+
log.info("%s Sending RAG info update: title='%s', sources=%d, is_complete=%s",
|
|
550
|
+
log_identifier, title[:50], len(sources), is_complete)
|
|
551
|
+
|
|
552
|
+
# Import and create the RAG info update data
|
|
553
|
+
from ...common.data_parts import RAGInfoUpdateData
|
|
554
|
+
|
|
555
|
+
rag_info_data = RAGInfoUpdateData(
|
|
556
|
+
title=title,
|
|
557
|
+
query=citation_tracker.research_question,
|
|
558
|
+
search_type="deep_research",
|
|
559
|
+
sources=sources,
|
|
560
|
+
is_complete=is_complete,
|
|
561
|
+
timestamp=datetime.now(timezone.utc).isoformat()
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Use the host component's helper method to publish the data signal
|
|
565
|
+
host_component.publish_data_signal_from_thread(
|
|
566
|
+
a2a_context=a2a_context,
|
|
567
|
+
signal_data=rag_info_data,
|
|
568
|
+
skip_buffer_flush=False,
|
|
569
|
+
log_identifier=log_identifier,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
except Exception as e:
|
|
573
|
+
log.error("%s Error sending RAG info update: %s", log_identifier, str(e))
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
async def _send_deep_research_report_signal(
|
|
577
|
+
artifact_filename: str,
|
|
578
|
+
artifact_version: int,
|
|
579
|
+
title: str,
|
|
580
|
+
sources_count: int,
|
|
581
|
+
tool_context: ToolContext
|
|
582
|
+
) -> None:
|
|
583
|
+
"""
|
|
584
|
+
Send DeepResearchReportData signal directly to frontend.
|
|
585
|
+
|
|
586
|
+
This bypasses the LLM response entirely, ensuring the report is displayed
|
|
587
|
+
via the DeepResearchReportBubble component without duplication.
|
|
588
|
+
|
|
589
|
+
The frontend will receive this signal and render the report using the
|
|
590
|
+
artifact viewer, suppressing any text content from the LLM response.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
artifact_filename: The filename of the research report artifact
|
|
594
|
+
artifact_version: The version number of the artifact
|
|
595
|
+
title: Human-readable title for the research
|
|
596
|
+
sources_count: Number of sources analyzed
|
|
597
|
+
tool_context: Tool context for accessing agent
|
|
598
|
+
"""
|
|
599
|
+
log_identifier = "[DeepResearch:ReportSignal]"
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
# Get a2a context from tool context state
|
|
603
|
+
a2a_context = tool_context.state.get("a2a_context")
|
|
604
|
+
if not a2a_context:
|
|
605
|
+
log.warning("%s No a2a_context found, cannot send report signal", log_identifier)
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
# Get the host component from invocation context
|
|
609
|
+
invocation_context = getattr(tool_context, '_invocation_context', None)
|
|
610
|
+
if not invocation_context:
|
|
611
|
+
log.warning("%s No invocation context found", log_identifier)
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
agent = getattr(invocation_context, 'agent', None)
|
|
615
|
+
if not agent:
|
|
616
|
+
log.warning("%s No agent found in invocation context", log_identifier)
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
host_component = getattr(agent, 'host_component', None)
|
|
620
|
+
if not host_component:
|
|
621
|
+
log.warning("%s No host component found on agent", log_identifier)
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
# Build the artifact URI for the frontend
|
|
625
|
+
# Format: artifact://{session_id}/{filename}?version={version}
|
|
626
|
+
# This matches the format expected by parseArtifactUri in download.ts
|
|
627
|
+
from ..utils.context_helpers import get_original_session_id
|
|
628
|
+
session_id = get_original_session_id(invocation_context)
|
|
629
|
+
artifact_uri = f"artifact://{session_id}/{artifact_filename}?version={artifact_version}"
|
|
630
|
+
|
|
631
|
+
log.info("%s Sending deep research report signal: filename='%s', version=%d, uri='%s'",
|
|
632
|
+
log_identifier, artifact_filename, artifact_version, artifact_uri)
|
|
633
|
+
|
|
634
|
+
# Import and create the DeepResearchReportData
|
|
635
|
+
from ...common.data_parts import DeepResearchReportData
|
|
636
|
+
|
|
637
|
+
report_data = DeepResearchReportData(
|
|
638
|
+
filename=artifact_filename,
|
|
639
|
+
version=artifact_version,
|
|
640
|
+
uri=artifact_uri,
|
|
641
|
+
title=title,
|
|
642
|
+
sources_count=sources_count
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Use the host component's helper method to publish the data signal
|
|
646
|
+
host_component.publish_data_signal_from_thread(
|
|
647
|
+
a2a_context=a2a_context,
|
|
648
|
+
signal_data=report_data,
|
|
649
|
+
skip_buffer_flush=False,
|
|
650
|
+
log_identifier=log_identifier,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
log.info("%s Successfully sent deep research report signal", log_identifier)
|
|
654
|
+
|
|
655
|
+
except Exception as e:
|
|
656
|
+
log.error("%s Error sending deep research report signal: %s", log_identifier, str(e))
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
async def _search_web(
|
|
660
|
+
query: str,
|
|
661
|
+
max_results: int,
|
|
662
|
+
tool_context: ToolContext,
|
|
663
|
+
tool_config: Optional[Dict[str, Any]],
|
|
664
|
+
send_progress: bool = True
|
|
665
|
+
) -> List[SearchResult]:
|
|
666
|
+
"""Search web using Google Custom Search API.
|
|
667
|
+
|
|
668
|
+
Note: For other search providers (Tavily, Exa, Brave), use the corresponding
|
|
669
|
+
plugins from the solace-agent-mesh-plugins repository.
|
|
670
|
+
"""
|
|
671
|
+
log_identifier = "[DeepResearch:WebSearch]"
|
|
672
|
+
|
|
673
|
+
if send_progress:
|
|
674
|
+
await _send_research_progress(
|
|
675
|
+
f"Searching web for: {query[:60]}...",
|
|
676
|
+
tool_context
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
log.info("%s Attempting Google search", log_identifier)
|
|
681
|
+
result = await web_search_google(
|
|
682
|
+
query=query,
|
|
683
|
+
max_results=max_results,
|
|
684
|
+
tool_context=tool_context,
|
|
685
|
+
tool_config=tool_config
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
if isinstance(result, dict) and result.get("result"):
|
|
689
|
+
result_data = json.loads(result["result"])
|
|
690
|
+
search_results = []
|
|
691
|
+
|
|
692
|
+
for item in result_data.get("organic", []):
|
|
693
|
+
search_results.append(SearchResult(
|
|
694
|
+
source_type="web",
|
|
695
|
+
title=item.get("title", ""),
|
|
696
|
+
content=item.get("snippet", ""),
|
|
697
|
+
url=item.get("link", ""),
|
|
698
|
+
relevance_score=0.85,
|
|
699
|
+
metadata={"provider": "google"}
|
|
700
|
+
))
|
|
701
|
+
|
|
702
|
+
log.info("%s Found %d Google results", log_identifier, len(search_results))
|
|
703
|
+
return search_results
|
|
704
|
+
except Exception as e:
|
|
705
|
+
log.error("%s Google search failed: %s", log_identifier, str(e))
|
|
706
|
+
|
|
707
|
+
log.warning("%s No web search results available - Google search failed or not configured", log_identifier)
|
|
708
|
+
return []
|
|
709
|
+
|
|
710
|
+
# TODO: will add other sources such as knowledgebases
|
|
711
|
+
async def _multi_source_search(
|
|
712
|
+
query: str,
|
|
713
|
+
sources: List[str],
|
|
714
|
+
max_results_per_source: int,
|
|
715
|
+
kb_ids: Optional[List[str]],
|
|
716
|
+
tool_context: ToolContext,
|
|
717
|
+
tool_config: Optional[Dict[str, Any]]
|
|
718
|
+
) -> List[SearchResult]:
|
|
719
|
+
"""Execute search across various sources in parallel (web-only version)"""
|
|
720
|
+
log_identifier = "[DeepResearch:MultiSearch]"
|
|
721
|
+
log.info("%s Searching across sources: %s", log_identifier, sources)
|
|
722
|
+
|
|
723
|
+
tasks = []
|
|
724
|
+
|
|
725
|
+
# Web-only version - only web search
|
|
726
|
+
if "web" in sources:
|
|
727
|
+
tasks.append(_search_web(query, max_results_per_source, tool_context, tool_config, send_progress=False))
|
|
728
|
+
|
|
729
|
+
# Execute all searches in parallel
|
|
730
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
731
|
+
|
|
732
|
+
# Flatten and filter results
|
|
733
|
+
all_results = []
|
|
734
|
+
for result in results:
|
|
735
|
+
if isinstance(result, list):
|
|
736
|
+
all_results.extend(result)
|
|
737
|
+
elif isinstance(result, Exception):
|
|
738
|
+
log.warning("%s Search task failed: %s", log_identifier, str(result))
|
|
739
|
+
|
|
740
|
+
# Deduplicate by URL/title
|
|
741
|
+
seen = set()
|
|
742
|
+
unique_results = []
|
|
743
|
+
for result in all_results:
|
|
744
|
+
# For web sources, use URL or title as the key
|
|
745
|
+
key = result.url or f"web:{result.title}"
|
|
746
|
+
|
|
747
|
+
if key not in seen:
|
|
748
|
+
seen.add(key)
|
|
749
|
+
unique_results.append(result)
|
|
750
|
+
|
|
751
|
+
# Sort by relevance score
|
|
752
|
+
unique_results.sort(key=lambda x: x.relevance_score, reverse=True)
|
|
753
|
+
|
|
754
|
+
log.info("%s Found %d unique results across all sources", log_identifier, len(unique_results))
|
|
755
|
+
return unique_results
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
async def _generate_initial_queries(
|
|
759
|
+
research_question: str,
|
|
760
|
+
tool_context: ToolContext,
|
|
761
|
+
tool_config: Optional[Dict[str, Any]] = None
|
|
762
|
+
) -> List[str]:
|
|
763
|
+
"""
|
|
764
|
+
Generate 3-5 initial search queries using LLM.
|
|
765
|
+
The LLM breaks down the research question into effective search queries.
|
|
766
|
+
|
|
767
|
+
Supports phase-specific model via tool_config.
|
|
768
|
+
"""
|
|
769
|
+
log_identifier = "[DeepResearch:QueryGen]"
|
|
770
|
+
|
|
771
|
+
try:
|
|
772
|
+
# Get phase-specific or default model
|
|
773
|
+
llm = _get_model_for_phase("query_generation", tool_context, tool_config)
|
|
774
|
+
|
|
775
|
+
query_prompt = f"""You are a research query specialist. Generate 3-5 effective search queries to comprehensively research this question:
|
|
776
|
+
|
|
777
|
+
Research Question: {research_question}
|
|
778
|
+
|
|
779
|
+
Generate queries that:
|
|
780
|
+
1. Cover different aspects of the topic
|
|
781
|
+
2. Use varied terminology and perspectives
|
|
782
|
+
3. Range from broad to specific
|
|
783
|
+
4. Are optimized for search engines
|
|
784
|
+
|
|
785
|
+
Respond in JSON format:
|
|
786
|
+
{{
|
|
787
|
+
"queries": ["query1", "query2", "query3", "query4", "query5"]
|
|
788
|
+
}}"""
|
|
789
|
+
|
|
790
|
+
log.info("%s Calling LLM for query generation", log_identifier)
|
|
791
|
+
|
|
792
|
+
# Create LLM request
|
|
793
|
+
# Note: max_output_tokens=8192 to ensure complete JSON responses with "thinking" models
|
|
794
|
+
llm_request = LlmRequest(
|
|
795
|
+
model=llm.model,
|
|
796
|
+
contents=[adk_types.Content(role="user", parts=[adk_types.Part(text=query_prompt)])],
|
|
797
|
+
config=adk_types.GenerateContentConfig(
|
|
798
|
+
response_mime_type="application/json",
|
|
799
|
+
temperature=0.7,
|
|
800
|
+
max_output_tokens=8192
|
|
801
|
+
)
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
# Call LLM
|
|
805
|
+
if hasattr(llm, 'generate_content_async'):
|
|
806
|
+
async for response_event in llm.generate_content_async(llm_request):
|
|
807
|
+
response = response_event
|
|
808
|
+
break
|
|
809
|
+
else:
|
|
810
|
+
response = llm.generate_content(request=llm_request)
|
|
811
|
+
|
|
812
|
+
# Extract text from response using helper function
|
|
813
|
+
response_text = _extract_text_from_llm_response(response, log_identifier)
|
|
814
|
+
if not response_text or not response_text.strip():
|
|
815
|
+
return [research_question]
|
|
816
|
+
|
|
817
|
+
log.debug("%s LLM response text (first 200 chars): %s", log_identifier, response_text[:200])
|
|
818
|
+
|
|
819
|
+
# Parse JSON using helper function with fallback key
|
|
820
|
+
query_data = _parse_json_from_llm_response(response_text, log_identifier, fallback_key="queries")
|
|
821
|
+
if query_data is None:
|
|
822
|
+
return [research_question]
|
|
823
|
+
|
|
824
|
+
queries = query_data.get("queries", [research_question])[:5]
|
|
825
|
+
|
|
826
|
+
log.info("%s Generated %d queries via LLM", log_identifier, len(queries))
|
|
827
|
+
return queries
|
|
828
|
+
|
|
829
|
+
except Exception as e:
|
|
830
|
+
log.error("%s LLM query generation failed: %s, using fallback", log_identifier, str(e), exc_info=True)
|
|
831
|
+
return [research_question]
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
async def _generate_research_title(
|
|
835
|
+
research_question: str,
|
|
836
|
+
tool_context: ToolContext,
|
|
837
|
+
tool_config: Optional[Dict[str, Any]] = None
|
|
838
|
+
) -> str:
|
|
839
|
+
"""
|
|
840
|
+
Generate a concise, human-readable title for the research using LLM.
|
|
841
|
+
|
|
842
|
+
The LLM converts the research question into a short, descriptive title
|
|
843
|
+
suitable for display in the UI.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
research_question: The original research question
|
|
847
|
+
tool_context: Tool context for accessing agent
|
|
848
|
+
tool_config: Optional tool configuration
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
A concise title string (typically 5-10 words)
|
|
852
|
+
"""
|
|
853
|
+
log_identifier = "[DeepResearch:TitleGen]"
|
|
854
|
+
|
|
855
|
+
try:
|
|
856
|
+
# Get phase-specific or default model (use query_generation model for efficiency)
|
|
857
|
+
llm = _get_model_for_phase("query_generation", tool_context, tool_config)
|
|
858
|
+
|
|
859
|
+
title_prompt = f"""Generate a concise, human-readable title for this research topic.
|
|
860
|
+
|
|
861
|
+
Research Question: {research_question}
|
|
862
|
+
|
|
863
|
+
Requirements:
|
|
864
|
+
1. The title should be 5-10 words maximum
|
|
865
|
+
2. It should capture the essence of the research topic
|
|
866
|
+
3. It should be suitable for display as a heading
|
|
867
|
+
4. Do NOT include quotes around the title
|
|
868
|
+
5. Do NOT include "Research:" or similar prefixes
|
|
869
|
+
|
|
870
|
+
Respond with ONLY the title, nothing else."""
|
|
871
|
+
|
|
872
|
+
# Note: max_output_tokens=2048 to ensure complete responses with "thinking" models
|
|
873
|
+
llm_request = LlmRequest(
|
|
874
|
+
model=llm.model,
|
|
875
|
+
contents=[adk_types.Content(role="user", parts=[adk_types.Part(text=title_prompt)])],
|
|
876
|
+
config=adk_types.GenerateContentConfig(
|
|
877
|
+
temperature=0.3,
|
|
878
|
+
max_output_tokens=2048
|
|
879
|
+
)
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# Call LLM
|
|
883
|
+
response = None
|
|
884
|
+
if hasattr(llm, 'generate_content_async'):
|
|
885
|
+
async for response_event in llm.generate_content_async(llm_request):
|
|
886
|
+
response = response_event
|
|
887
|
+
break
|
|
888
|
+
else:
|
|
889
|
+
response = llm.generate_content(request=llm_request)
|
|
890
|
+
|
|
891
|
+
# Extract text from response using helper function
|
|
892
|
+
response_text = _extract_text_from_llm_response(response, log_identifier)
|
|
893
|
+
|
|
894
|
+
# Clean up the title
|
|
895
|
+
title = response_text.strip().strip('"').strip("'")
|
|
896
|
+
|
|
897
|
+
# Fallback if title is too long or empty
|
|
898
|
+
if not title or len(title) > 100:
|
|
899
|
+
# Use first 60 chars of research question as fallback
|
|
900
|
+
title = research_question[:60] + "..." if len(research_question) > 60 else research_question
|
|
901
|
+
|
|
902
|
+
log.info("%s Generated title: '%s'", log_identifier, title)
|
|
903
|
+
return title
|
|
904
|
+
|
|
905
|
+
except Exception as e:
|
|
906
|
+
log.error("%s LLM title generation failed: %s, using fallback", log_identifier, str(e))
|
|
907
|
+
# Fallback: use truncated research question
|
|
908
|
+
return research_question[:60] + "..." if len(research_question) > 60 else research_question
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def _prepare_findings_summary(findings: List[SearchResult], max_findings: int = 20) -> str:
|
|
912
|
+
"""Prepare a concise summary of findings for LLM reflection"""
|
|
913
|
+
if not findings:
|
|
914
|
+
return "No findings yet."
|
|
915
|
+
|
|
916
|
+
# Group by source type
|
|
917
|
+
by_type = {}
|
|
918
|
+
for finding in findings:
|
|
919
|
+
if finding.source_type not in by_type:
|
|
920
|
+
by_type[finding.source_type] = []
|
|
921
|
+
by_type[finding.source_type].append(finding)
|
|
922
|
+
|
|
923
|
+
summary_parts = []
|
|
924
|
+
summary_parts.append(f"Total Sources: {len(findings)}")
|
|
925
|
+
summary_parts.append(f"Source Types: {', '.join(by_type.keys())}")
|
|
926
|
+
summary_parts.append("")
|
|
927
|
+
|
|
928
|
+
# Add top findings from each source type
|
|
929
|
+
for source_type, type_findings in by_type.items():
|
|
930
|
+
summary_parts.append(f"{source_type.upper()} Sources ({len(type_findings)}):")
|
|
931
|
+
|
|
932
|
+
# Show top 5 from each type
|
|
933
|
+
for i, finding in enumerate(sorted(type_findings, key=lambda x: x.relevance_score, reverse=True)[:5], 1):
|
|
934
|
+
title = finding.title[:80] + "..." if len(finding.title) > 80 else finding.title
|
|
935
|
+
content = finding.content[:150] + "..." if len(finding.content) > 150 else finding.content
|
|
936
|
+
summary_parts.append(f" {i}. {title}")
|
|
937
|
+
summary_parts.append(f" {content}")
|
|
938
|
+
summary_parts.append(f" Relevance: {finding.relevance_score:.2f}")
|
|
939
|
+
summary_parts.append("")
|
|
940
|
+
|
|
941
|
+
return "\n".join(summary_parts)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
async def _reflect_on_findings(
|
|
945
|
+
research_question: str,
|
|
946
|
+
findings: List[SearchResult],
|
|
947
|
+
iteration: int,
|
|
948
|
+
tool_context: ToolContext,
|
|
949
|
+
max_iterations: int = 10,
|
|
950
|
+
tool_config: Optional[Dict[str, Any]] = None
|
|
951
|
+
) -> ReflectionResult:
|
|
952
|
+
"""
|
|
953
|
+
Reflect on current findings using LLM to determine next steps.
|
|
954
|
+
|
|
955
|
+
The LLM analyzes the research findings to:
|
|
956
|
+
1. Assess information completeness and quality
|
|
957
|
+
2. Identify knowledge gaps
|
|
958
|
+
3. Determine if more research is needed
|
|
959
|
+
4. Generate refined search queries
|
|
960
|
+
|
|
961
|
+
Supports phase-specific model via tool_config.
|
|
962
|
+
"""
|
|
963
|
+
log_identifier = "[DeepResearch:Reflection]"
|
|
964
|
+
|
|
965
|
+
try:
|
|
966
|
+
# Get phase-specific or default model
|
|
967
|
+
llm = _get_model_for_phase("reflection", tool_context, tool_config)
|
|
968
|
+
|
|
969
|
+
# Prepare findings summary for LLM
|
|
970
|
+
findings_summary = _prepare_findings_summary(findings)
|
|
971
|
+
|
|
972
|
+
# Create reflection prompt
|
|
973
|
+
reflection_prompt = f"""You are a research quality analyst. Analyze the current research findings and provide guidance for the next research iteration.
|
|
974
|
+
|
|
975
|
+
Research Question: {research_question}
|
|
976
|
+
|
|
977
|
+
Current Iteration: {iteration}
|
|
978
|
+
|
|
979
|
+
Findings Summary:
|
|
980
|
+
{findings_summary}
|
|
981
|
+
|
|
982
|
+
Please analyze these findings and provide:
|
|
983
|
+
|
|
984
|
+
1. **Quality Score** (0.0 to 1.0): How complete and comprehensive is the current research?
|
|
985
|
+
- 0.0-0.3: Very incomplete, major gaps
|
|
986
|
+
- 0.4-0.6: Partial coverage, significant gaps remain
|
|
987
|
+
- 0.7-0.8: Good coverage, minor gaps
|
|
988
|
+
- 0.9-1.0: Comprehensive, excellent coverage
|
|
989
|
+
|
|
990
|
+
2. **Knowledge Gaps**: What important aspects are missing or under-covered?
|
|
991
|
+
|
|
992
|
+
3. **Should Continue**: Should we conduct another research iteration? (yes/no)
|
|
993
|
+
- Consider: quality score, iteration number, diminishing returns
|
|
994
|
+
- Maximum iterations allowed: {max_iterations}
|
|
995
|
+
|
|
996
|
+
4. **Suggested Queries**: If continuing, what 3-5 specific search queries would fill the gaps?
|
|
997
|
+
|
|
998
|
+
Respond in JSON format:
|
|
999
|
+
{{
|
|
1000
|
+
"quality_score": 0.0-1.0,
|
|
1001
|
+
"gaps": ["gap1", "gap2", ...],
|
|
1002
|
+
"should_continue": true/false,
|
|
1003
|
+
"suggested_queries": ["query1", "query2", ...],
|
|
1004
|
+
"reasoning": "Brief explanation of your assessment"
|
|
1005
|
+
}}"""
|
|
1006
|
+
|
|
1007
|
+
log.info("%s Calling LLM for reflection analysis", log_identifier)
|
|
1008
|
+
|
|
1009
|
+
# Create LLM request
|
|
1010
|
+
# Note: max_output_tokens=8192 to ensure complete JSON responses with "thinking" models
|
|
1011
|
+
llm_request = LlmRequest(
|
|
1012
|
+
model=llm.model,
|
|
1013
|
+
contents=[adk_types.Content(role="user", parts=[adk_types.Part(text=reflection_prompt)])],
|
|
1014
|
+
config=adk_types.GenerateContentConfig(
|
|
1015
|
+
response_mime_type="application/json",
|
|
1016
|
+
temperature=0.3,
|
|
1017
|
+
max_output_tokens=8192
|
|
1018
|
+
)
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Call LLM
|
|
1022
|
+
if hasattr(llm, 'generate_content_async'):
|
|
1023
|
+
async for response_event in llm.generate_content_async(llm_request):
|
|
1024
|
+
response = response_event
|
|
1025
|
+
break
|
|
1026
|
+
else:
|
|
1027
|
+
response = llm.generate_content(request=llm_request)
|
|
1028
|
+
|
|
1029
|
+
# Extract text from response using helper function
|
|
1030
|
+
response_text = _extract_text_from_llm_response(response, log_identifier)
|
|
1031
|
+
if not response_text or not response_text.strip():
|
|
1032
|
+
log.warning("%s LLM returned empty response for reflection", log_identifier)
|
|
1033
|
+
# Continue research if we have few findings
|
|
1034
|
+
should_continue = len(findings) < 15 and iteration < 3
|
|
1035
|
+
return ReflectionResult(
|
|
1036
|
+
quality_score=0.6,
|
|
1037
|
+
gaps=["Need more sources"],
|
|
1038
|
+
should_continue=should_continue,
|
|
1039
|
+
suggested_queries=[f"{research_question} detailed analysis", f"{research_question} comprehensive overview"],
|
|
1040
|
+
reasoning="LLM returned empty response, using fallback logic"
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
# Parse JSON using helper function
|
|
1044
|
+
reflection_data = _parse_json_from_llm_response(response_text, log_identifier, fallback_key="quality_score")
|
|
1045
|
+
if reflection_data is None:
|
|
1046
|
+
should_continue = len(findings) < 15 and iteration < 3
|
|
1047
|
+
return ReflectionResult(
|
|
1048
|
+
quality_score=0.6,
|
|
1049
|
+
gaps=["Need more sources"],
|
|
1050
|
+
should_continue=should_continue,
|
|
1051
|
+
suggested_queries=[f"{research_question} comprehensive", f"{research_question} detailed"],
|
|
1052
|
+
reasoning="Could not parse LLM response, using fallback"
|
|
1053
|
+
)
|
|
1054
|
+
|
|
1055
|
+
quality_score = float(reflection_data.get("quality_score", 0.5))
|
|
1056
|
+
gaps = reflection_data.get("gaps", [])
|
|
1057
|
+
should_continue = reflection_data.get("should_continue", False) and iteration < max_iterations
|
|
1058
|
+
suggested_queries = reflection_data.get("suggested_queries", [])
|
|
1059
|
+
reasoning = reflection_data.get("reasoning", "LLM reflection completed")
|
|
1060
|
+
|
|
1061
|
+
log.info("%s LLM Reflection - Quality: %.2f, Continue: %s",
|
|
1062
|
+
log_identifier, quality_score, should_continue)
|
|
1063
|
+
log.info("%s Reasoning: %s", log_identifier, reasoning)
|
|
1064
|
+
|
|
1065
|
+
return ReflectionResult(
|
|
1066
|
+
quality_score=quality_score,
|
|
1067
|
+
gaps=gaps,
|
|
1068
|
+
should_continue=should_continue,
|
|
1069
|
+
suggested_queries=suggested_queries[:5] if suggested_queries else [research_question],
|
|
1070
|
+
reasoning=reasoning
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
log.error("%s LLM reflection failed: %s", log_identifier, str(e))
|
|
1075
|
+
# Fallback: continue if we don't have many findings yet
|
|
1076
|
+
should_continue = len(findings) < 15 and iteration < 3
|
|
1077
|
+
return ReflectionResult(
|
|
1078
|
+
quality_score=0.5,
|
|
1079
|
+
gaps=["LLM reflection error"],
|
|
1080
|
+
should_continue=should_continue,
|
|
1081
|
+
suggested_queries=[f"{research_question} overview"] if should_continue else [],
|
|
1082
|
+
reasoning=f"Error during reflection: {str(e)}"
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
async def _select_sources_to_fetch(
|
|
1087
|
+
research_question: str,
|
|
1088
|
+
findings: List[SearchResult],
|
|
1089
|
+
max_to_fetch: int,
|
|
1090
|
+
tool_context: ToolContext,
|
|
1091
|
+
tool_config: Optional[Dict[str, Any]] = None
|
|
1092
|
+
) -> List[SearchResult]:
|
|
1093
|
+
"""
|
|
1094
|
+
Use LLM to intelligently select which sources to fetch based on quality and relevance.
|
|
1095
|
+
|
|
1096
|
+
Supports phase-specific model via tool_config.
|
|
1097
|
+
"""
|
|
1098
|
+
log_identifier = "[DeepResearch:SelectSources]"
|
|
1099
|
+
|
|
1100
|
+
try:
|
|
1101
|
+
# Get phase-specific or default model
|
|
1102
|
+
llm = _get_model_for_phase("source_selection", tool_context, tool_config)
|
|
1103
|
+
|
|
1104
|
+
# Prepare source list for LLM - only web sources can be fetched for full content
|
|
1105
|
+
web_findings = [f for f in findings if f.source_type == "web" and f.url]
|
|
1106
|
+
if not web_findings:
|
|
1107
|
+
return []
|
|
1108
|
+
|
|
1109
|
+
sources_summary = []
|
|
1110
|
+
for i, finding in enumerate(web_findings[:20], 1): # Limit to top 20 for LLM
|
|
1111
|
+
sources_summary.append(f"{i}. {finding.title}")
|
|
1112
|
+
sources_summary.append(f" URL: {finding.url}")
|
|
1113
|
+
sources_summary.append(f" Snippet: {finding.content[:150]}...")
|
|
1114
|
+
sources_summary.append(f" Relevance: {finding.relevance_score:.2f}")
|
|
1115
|
+
sources_summary.append("")
|
|
1116
|
+
|
|
1117
|
+
selection_prompt = f"""You are a research quality analyst. Select the {max_to_fetch} BEST sources to fetch full content from for this research question:
|
|
1118
|
+
|
|
1119
|
+
Research Question: {research_question}
|
|
1120
|
+
|
|
1121
|
+
Available Sources:
|
|
1122
|
+
{chr(10).join(sources_summary)}
|
|
1123
|
+
|
|
1124
|
+
Select the {max_to_fetch} sources that are most likely to provide:
|
|
1125
|
+
1. Authoritative, credible information (e.g., .edu, .gov, established organizations)
|
|
1126
|
+
2. Comprehensive coverage of the topic
|
|
1127
|
+
3. Unique perspectives or data
|
|
1128
|
+
4. Academic or expert analysis
|
|
1129
|
+
|
|
1130
|
+
You MUST respond with ONLY valid JSON in this exact format:
|
|
1131
|
+
{{
|
|
1132
|
+
"selected_sources": [1, 3, 5],
|
|
1133
|
+
"reasoning": "Brief explanation"
|
|
1134
|
+
}}
|
|
1135
|
+
|
|
1136
|
+
Do not include any other text, markdown formatting, or explanations outside the JSON."""
|
|
1137
|
+
|
|
1138
|
+
# Note: max_output_tokens=8192 to ensure complete JSON responses with "thinking" models
|
|
1139
|
+
llm_request = LlmRequest(
|
|
1140
|
+
model=llm.model,
|
|
1141
|
+
contents=[adk_types.Content(role="user", parts=[adk_types.Part(text=selection_prompt)])],
|
|
1142
|
+
config=adk_types.GenerateContentConfig(
|
|
1143
|
+
response_mime_type="application/json",
|
|
1144
|
+
temperature=0.3,
|
|
1145
|
+
max_output_tokens=8192
|
|
1146
|
+
)
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
if hasattr(llm, 'generate_content_async'):
|
|
1150
|
+
async for response_event in llm.generate_content_async(llm_request):
|
|
1151
|
+
response = response_event
|
|
1152
|
+
break
|
|
1153
|
+
else:
|
|
1154
|
+
response = llm.generate_content(request=llm_request)
|
|
1155
|
+
|
|
1156
|
+
# Extract text from response using helper function
|
|
1157
|
+
response_text = _extract_text_from_llm_response(response, log_identifier)
|
|
1158
|
+
if not response_text or not response_text.strip():
|
|
1159
|
+
log.warning("%s LLM returned empty response, using fallback selection", log_identifier)
|
|
1160
|
+
web_findings = [f for f in findings if f.source_type == "web" and f.url]
|
|
1161
|
+
return sorted(web_findings, key=lambda x: x.relevance_score, reverse=True)[:max_to_fetch]
|
|
1162
|
+
|
|
1163
|
+
log.debug("%s LLM response text: %s", log_identifier, response_text[:200])
|
|
1164
|
+
|
|
1165
|
+
# Parse JSON using helper function
|
|
1166
|
+
selection_data = _parse_json_from_llm_response(response_text, log_identifier, fallback_key="selected_sources")
|
|
1167
|
+
if selection_data is None:
|
|
1168
|
+
log.warning("%s Failed to parse JSON, using fallback selection", log_identifier)
|
|
1169
|
+
web_findings = [f for f in findings if f.source_type == "web" and f.url]
|
|
1170
|
+
return sorted(web_findings, key=lambda x: x.relevance_score, reverse=True)[:max_to_fetch]
|
|
1171
|
+
|
|
1172
|
+
selected_indices = selection_data.get("selected_sources", [])
|
|
1173
|
+
reasoning = selection_data.get("reasoning", "")
|
|
1174
|
+
|
|
1175
|
+
if not selected_indices:
|
|
1176
|
+
log.warning("%s LLM returned empty selection, using fallback", log_identifier)
|
|
1177
|
+
web_findings = [f for f in findings if f.source_type == "web" and f.url]
|
|
1178
|
+
return sorted(web_findings, key=lambda x: x.relevance_score, reverse=True)[:max_to_fetch]
|
|
1179
|
+
|
|
1180
|
+
log.info("%s LLM selected %d sources: %s", log_identifier, len(selected_indices), reasoning)
|
|
1181
|
+
|
|
1182
|
+
# Convert 1-based indices to actual findings
|
|
1183
|
+
selected_sources = []
|
|
1184
|
+
for idx in selected_indices:
|
|
1185
|
+
if 1 <= idx <= len(web_findings):
|
|
1186
|
+
selected_sources.append(web_findings[idx - 1])
|
|
1187
|
+
|
|
1188
|
+
return selected_sources[:max_to_fetch]
|
|
1189
|
+
|
|
1190
|
+
except Exception as e:
|
|
1191
|
+
log.error("%s LLM source selection failed: %s, using fallback", log_identifier, str(e), exc_info=True)
|
|
1192
|
+
web_findings = [f for f in findings if f.source_type == "web" and f.url]
|
|
1193
|
+
return sorted(web_findings, key=lambda x: x.relevance_score, reverse=True)[:max_to_fetch]
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
async def _fetch_selected_sources(
|
|
1197
|
+
selected_sources: List[SearchResult],
|
|
1198
|
+
tool_context: ToolContext,
|
|
1199
|
+
tool_config: Optional[Dict[str, Any]],
|
|
1200
|
+
citation_tracker: ResearchCitationTracker,
|
|
1201
|
+
start_time: float = 0,
|
|
1202
|
+
max_runtime_seconds: Optional[int] = None
|
|
1203
|
+
) -> Dict[str, int]:
|
|
1204
|
+
"""Fetch full content from LLM-selected sources and return success/failure stats"""
|
|
1205
|
+
log_identifier = "[DeepResearch:FetchSources]"
|
|
1206
|
+
|
|
1207
|
+
if not selected_sources:
|
|
1208
|
+
log.info("%s No sources selected to fetch", log_identifier)
|
|
1209
|
+
return {"success": 0, "failed": 0}
|
|
1210
|
+
|
|
1211
|
+
log.info("%s Fetching full content from %d selected sources", log_identifier, len(selected_sources))
|
|
1212
|
+
|
|
1213
|
+
# Fetch sources in parallel with progress updates
|
|
1214
|
+
fetch_tasks = []
|
|
1215
|
+
for i, source in enumerate(selected_sources, 1):
|
|
1216
|
+
# Prepare current URL being fetched for structured progress
|
|
1217
|
+
current_url_info = {
|
|
1218
|
+
"url": source.url,
|
|
1219
|
+
"title": source.title,
|
|
1220
|
+
"favicon": f"https://www.google.com/s2/favicons?domain={source.url}&sz=32" if source.url else ""
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
# Send progress for each source being fetched with phase info
|
|
1224
|
+
await _send_research_progress(
|
|
1225
|
+
f"Reading content from: {source.title[:50]}... ({i}/{len(selected_sources)})",
|
|
1226
|
+
tool_context,
|
|
1227
|
+
phase="analyzing"
|
|
1228
|
+
)
|
|
1229
|
+
fetch_tasks.append(web_request(
|
|
1230
|
+
url=source.url,
|
|
1231
|
+
method="GET",
|
|
1232
|
+
tool_context=tool_context,
|
|
1233
|
+
tool_config=tool_config
|
|
1234
|
+
))
|
|
1235
|
+
|
|
1236
|
+
results = await asyncio.gather(*fetch_tasks, return_exceptions=True)
|
|
1237
|
+
|
|
1238
|
+
# Track success/failure stats
|
|
1239
|
+
success_count = 0
|
|
1240
|
+
failed_count = 0
|
|
1241
|
+
|
|
1242
|
+
# Update findings with fetched content
|
|
1243
|
+
for source, result in zip(selected_sources, results):
|
|
1244
|
+
if isinstance(result, dict) and result.get("status") == "success":
|
|
1245
|
+
# Extract preview from result
|
|
1246
|
+
preview = result.get("result_preview", "")
|
|
1247
|
+
if preview:
|
|
1248
|
+
# Append fetched content to existing snippet
|
|
1249
|
+
source.content = f"{source.content}\n\n[Full Content Fetched]\n{preview}"
|
|
1250
|
+
source.metadata["fetched"] = True
|
|
1251
|
+
source.metadata["fetch_status"] = "success"
|
|
1252
|
+
success_count += 1
|
|
1253
|
+
log.info("%s Successfully fetched content from %s", log_identifier, source.url)
|
|
1254
|
+
|
|
1255
|
+
# Update citation tracker with fetched metadata
|
|
1256
|
+
citation_tracker.update_citation_after_fetch(source)
|
|
1257
|
+
else:
|
|
1258
|
+
source.metadata["fetched"] = False
|
|
1259
|
+
source.metadata["fetch_error"] = "No content in response"
|
|
1260
|
+
failed_count += 1
|
|
1261
|
+
log.warning("%s No content returned from %s", log_identifier, source.url)
|
|
1262
|
+
elif isinstance(result, Exception):
|
|
1263
|
+
log.warning("%s Failed to fetch %s: %s", log_identifier, source.url, str(result))
|
|
1264
|
+
source.metadata["fetched"] = False
|
|
1265
|
+
source.metadata["fetch_error"] = str(result)
|
|
1266
|
+
failed_count += 1
|
|
1267
|
+
else:
|
|
1268
|
+
error_msg = result.get("message", "Unknown error") if isinstance(result, dict) else "Unknown error"
|
|
1269
|
+
log.warning("%s Failed to fetch %s: %s", log_identifier, source.url, error_msg)
|
|
1270
|
+
source.metadata["fetched"] = False
|
|
1271
|
+
source.metadata["fetch_error"] = error_msg
|
|
1272
|
+
failed_count += 1
|
|
1273
|
+
|
|
1274
|
+
# Log summary
|
|
1275
|
+
log.info("%s Fetch complete: %d succeeded, %d failed out of %d total",
|
|
1276
|
+
log_identifier, success_count, failed_count, len(selected_sources))
|
|
1277
|
+
|
|
1278
|
+
# Send summary progress update
|
|
1279
|
+
if failed_count > 0:
|
|
1280
|
+
await _send_research_progress(
|
|
1281
|
+
f"Content fetched: {success_count} succeeded, {failed_count} failed",
|
|
1282
|
+
tool_context,
|
|
1283
|
+
phase="analyzing"
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
return {"success": success_count, "failed": failed_count}
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def _prepare_findings_for_report(findings: List[SearchResult], max_findings: int = 30) -> str:
|
|
1290
|
+
"""Prepare findings text for LLM report generation with enhanced content"""
|
|
1291
|
+
sorted_findings = sorted(findings, key=lambda x: x.relevance_score, reverse=True)[:max_findings]
|
|
1292
|
+
|
|
1293
|
+
findings_text = []
|
|
1294
|
+
findings_text.append("# Research Findings\n")
|
|
1295
|
+
|
|
1296
|
+
# Group findings by whether they have full content
|
|
1297
|
+
fetched_findings = [f for f in sorted_findings if f.metadata.get('fetched')]
|
|
1298
|
+
snippet_findings = [f for f in sorted_findings if not f.metadata.get('fetched')]
|
|
1299
|
+
|
|
1300
|
+
# Prioritize fetched content (full articles)
|
|
1301
|
+
if fetched_findings:
|
|
1302
|
+
findings_text.append("## Detailed Sources (Full Content Retrieved)\n")
|
|
1303
|
+
for finding in fetched_findings[:15]: # Top 15 fetched sources
|
|
1304
|
+
findings_text.append(f"\n### {finding.title}")
|
|
1305
|
+
findings_text.append(f"**Citation ID:** {finding.citation_id}")
|
|
1306
|
+
findings_text.append(f"**URL:** {finding.url or 'N/A'}")
|
|
1307
|
+
findings_text.append(f"**Relevance:** {finding.relevance_score:.2f}\n")
|
|
1308
|
+
|
|
1309
|
+
# Include substantial content from fetched sources (up to 5000 chars for comprehensive analysis)
|
|
1310
|
+
content_to_include = finding.content[:5000] if len(finding.content) > 5000 else finding.content
|
|
1311
|
+
if len(finding.content) > 5000:
|
|
1312
|
+
content_to_include += "\n\n[Content continues but truncated for length...]"
|
|
1313
|
+
findings_text.append(f"**Content:**\n{content_to_include}\n")
|
|
1314
|
+
findings_text.append("---\n")
|
|
1315
|
+
|
|
1316
|
+
# Add snippet-only sources
|
|
1317
|
+
if snippet_findings:
|
|
1318
|
+
findings_text.append("\n## Additional Sources (Snippets)\n")
|
|
1319
|
+
for finding in snippet_findings[:15]: # Top 15 snippet sources
|
|
1320
|
+
findings_text.append(f"\n### {finding.title}")
|
|
1321
|
+
findings_text.append(f"**Citation ID:** {finding.citation_id}")
|
|
1322
|
+
findings_text.append(f"**URL:** {finding.url or 'N/A'}")
|
|
1323
|
+
findings_text.append(f"**Snippet:** {finding.content}")
|
|
1324
|
+
findings_text.append(f"**Relevance:** {finding.relevance_score:.2f}\n")
|
|
1325
|
+
findings_text.append("---\n")
|
|
1326
|
+
|
|
1327
|
+
return "\n".join(findings_text)
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _generate_sources_section(all_findings: List[SearchResult]) -> str:
|
|
1331
|
+
"""Generate references section with ALL cited sources (both fetched and snippet-only)"""
|
|
1332
|
+
# Include ALL sources that have citation IDs (all findings that were cited)
|
|
1333
|
+
cited_sources = [f for f in all_findings if f.citation_id]
|
|
1334
|
+
|
|
1335
|
+
if not cited_sources:
|
|
1336
|
+
return ""
|
|
1337
|
+
|
|
1338
|
+
# Separate fetched vs snippet-only for better organization
|
|
1339
|
+
fetched_sources = [f for f in cited_sources if f.metadata.get('fetched')]
|
|
1340
|
+
snippet_sources = [f for f in cited_sources if not f.metadata.get('fetched')]
|
|
1341
|
+
|
|
1342
|
+
section = "\n\n---\n\n## References\n\n"
|
|
1343
|
+
|
|
1344
|
+
# Group by source type
|
|
1345
|
+
web_sources = [f for f in cited_sources if f.source_type == "web"]
|
|
1346
|
+
kb_sources = [f for f in cited_sources if f.source_type == "kb"]
|
|
1347
|
+
|
|
1348
|
+
if web_sources:
|
|
1349
|
+
for i, source in enumerate(web_sources, 1):
|
|
1350
|
+
if source.citation_id and source.url:
|
|
1351
|
+
# Extract citation number from citation_id (e.g., "search0" -> 0)
|
|
1352
|
+
citation_num = int(source.citation_id.replace("search", "").replace("file", "").replace("ref", ""))
|
|
1353
|
+
display_num = citation_num + 1 # Convert 0-based to 1-based for display
|
|
1354
|
+
|
|
1355
|
+
# DEBUG: Log citation mapping
|
|
1356
|
+
log.info("[DeepResearch:References] Mapping citation_id=%s to reference number [%d]", source.citation_id, display_num)
|
|
1357
|
+
|
|
1358
|
+
# Indicate if this was read in full or just a snippet
|
|
1359
|
+
fetch_indicator = " *(read in full)*" if source.metadata.get('fetched') else " *(search result)*"
|
|
1360
|
+
section += f"**[{display_num}]** {source.title}{fetch_indicator} \n{source.url}\n\n"
|
|
1361
|
+
|
|
1362
|
+
if kb_sources:
|
|
1363
|
+
for source in kb_sources:
|
|
1364
|
+
if source.citation_id:
|
|
1365
|
+
# Extract citation number from citation_id
|
|
1366
|
+
citation_num = int(source.citation_id.replace("search", "").replace("file", "").replace("ref", ""))
|
|
1367
|
+
display_num = citation_num + 1 # Convert 0-based to 1-based for display
|
|
1368
|
+
|
|
1369
|
+
fetch_indicator = " *(read in full)*" if source.metadata.get('fetched') else " *(search result)*"
|
|
1370
|
+
section += f"**[{display_num}]** {source.title}{fetch_indicator}\n\n"
|
|
1371
|
+
|
|
1372
|
+
return section
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _generate_methodology_section(all_findings: List[SearchResult]) -> str:
|
|
1376
|
+
"""Generate research methodology section with statistics"""
|
|
1377
|
+
web_sources = [f for f in all_findings if f.source_type == "web"]
|
|
1378
|
+
kb_sources = [f for f in all_findings if f.source_type == "kb"]
|
|
1379
|
+
|
|
1380
|
+
# Count fetched vs snippet-only sources
|
|
1381
|
+
fetched_sources = [f for f in all_findings if f.metadata.get('fetched')]
|
|
1382
|
+
snippet_sources = [f for f in all_findings if not f.metadata.get('fetched')]
|
|
1383
|
+
|
|
1384
|
+
section = "## Research Methodology\n\n"
|
|
1385
|
+
section += f"This research analyzed **{len(all_findings)} sources** across multiple iterations:\n\n"
|
|
1386
|
+
section += f"- **{len(fetched_sources)} sources** were read in full detail (cited in References above)\n"
|
|
1387
|
+
section += f"- **{len(snippet_sources)} additional sources** were consulted via search snippets\n"
|
|
1388
|
+
section += f"- Source types: {len(web_sources)} web, {len(kb_sources)} knowledge base\n\n"
|
|
1389
|
+
section += "The research process involved:\n"
|
|
1390
|
+
section += "1. Generating targeted search queries using AI\n"
|
|
1391
|
+
section += "2. Searching across multiple information sources\n"
|
|
1392
|
+
section += "3. Selecting the most authoritative and relevant sources\n"
|
|
1393
|
+
section += "4. Retrieving and analyzing full content from selected sources\n"
|
|
1394
|
+
section += "5. Synthesizing findings into a comprehensive report\n"
|
|
1395
|
+
|
|
1396
|
+
return section
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
async def _generate_research_report(
|
|
1400
|
+
research_question: str,
|
|
1401
|
+
all_findings: List[SearchResult],
|
|
1402
|
+
citation_tracker: ResearchCitationTracker,
|
|
1403
|
+
tool_context: ToolContext,
|
|
1404
|
+
tool_config: Optional[Dict[str, Any]] = None
|
|
1405
|
+
) -> str:
|
|
1406
|
+
"""
|
|
1407
|
+
Generate comprehensive research report using LLM.
|
|
1408
|
+
The LLM synthesizes findings into a coherent narrative with proper citations.
|
|
1409
|
+
|
|
1410
|
+
Supports phase-specific model via tool_config.
|
|
1411
|
+
"""
|
|
1412
|
+
log_identifier = "[DeepResearch:ReportGen]"
|
|
1413
|
+
log.info("%s Generating report from %d findings", log_identifier, len(all_findings))
|
|
1414
|
+
|
|
1415
|
+
try:
|
|
1416
|
+
# Get phase-specific or default model
|
|
1417
|
+
llm = _get_model_for_phase("report_generation", tool_context, tool_config)
|
|
1418
|
+
|
|
1419
|
+
# Prepare findings for LLM
|
|
1420
|
+
findings_text = _prepare_findings_for_report(all_findings)
|
|
1421
|
+
|
|
1422
|
+
# Create report generation prompt - emphasizing synthesis over copying
|
|
1423
|
+
report_prompt = f"""You are an expert research analyst. Your task is to SYNTHESIZE information from multiple sources into an original, comprehensive research report.
|
|
1424
|
+
|
|
1425
|
+
Research Question: {research_question}
|
|
1426
|
+
|
|
1427
|
+
You have access to {len(all_findings)} sources below. Your job is to READ ALL OF THEM, extract key information, and create a well-written report.
|
|
1428
|
+
|
|
1429
|
+
Source Materials:
|
|
1430
|
+
{findings_text}
|
|
1431
|
+
|
|
1432
|
+
CRITICAL INSTRUCTIONS:
|
|
1433
|
+
|
|
1434
|
+
⚠️ DO NOT COPY: You must NOT copy text directly from any single source. You must SYNTHESIZE information from MULTIPLE sources.
|
|
1435
|
+
|
|
1436
|
+
⚠️ ORIGINAL WRITING: Write in your own words, combining insights from different sources.
|
|
1437
|
+
|
|
1438
|
+
⚠️ DO NOT INCLUDE WORD COUNTS: Do NOT include word count targets (like "300-500 words") in your section headings or anywhere in the output. These are internal guidelines for you only.
|
|
1439
|
+
|
|
1440
|
+
REPORT STRUCTURE GUIDELINES (aim for 3000-5000 words total, but DO NOT mention word counts in output):
|
|
1441
|
+
|
|
1442
|
+
Write the following sections WITHOUT including word count targets in headings:
|
|
1443
|
+
|
|
1444
|
+
## Executive Summary
|
|
1445
|
+
Synthesize the MOST IMPORTANT insights from ALL sources. Highlight key findings that answer the research question. Provide context for why this topic matters. DO NOT copy from any single source.
|
|
1446
|
+
|
|
1447
|
+
## Introduction
|
|
1448
|
+
Explain the research question and its significance. Provide historical or contextual background. Outline what the report will cover. Draw context from multiple sources [[cite:searchX]].
|
|
1449
|
+
|
|
1450
|
+
## Main Analysis
|
|
1451
|
+
Organize into 5-8 thematic sections with descriptive headings (###). For EACH section:
|
|
1452
|
+
- Create a descriptive heading like "### Historical Development" or "### Economic Impact" (NO word counts)
|
|
1453
|
+
- Draw information from multiple sources
|
|
1454
|
+
- Start each paragraph with a topic sentence
|
|
1455
|
+
- Support claims with citations from different sources.[[cite:searchX]][[cite:searchY]]
|
|
1456
|
+
- Explain implications and connections
|
|
1457
|
+
- Compare and contrast different perspectives
|
|
1458
|
+
- NEVER copy paragraphs from a single source
|
|
1459
|
+
|
|
1460
|
+
## Comparative Analysis
|
|
1461
|
+
Compare different perspectives across sources. Identify agreements and contradictions. Analyze why sources might differ. Synthesize a balanced view. Cite multiple sources for each point.
|
|
1462
|
+
|
|
1463
|
+
## Implications
|
|
1464
|
+
Discuss practical implications. Identify applications or consequences. Suggest areas needing further research. Draw from multiple sources.
|
|
1465
|
+
|
|
1466
|
+
## Conclusion
|
|
1467
|
+
Synthesize the key takeaways from ALL sources. Provide final analytical insights. Suggest future directions.
|
|
1468
|
+
|
|
1469
|
+
⚠️ DO NOT CREATE A REFERENCES SECTION: The system will automatically append a properly formatted References section with all cited sources. Your report should end with the Conclusion section.
|
|
1470
|
+
|
|
1471
|
+
CITATION RULES:
|
|
1472
|
+
- Use [[cite:searchN]] format where N is the citation number from sources above
|
|
1473
|
+
- Place citations AFTER the period at the end of sentences (e.g., "This is a fact.[[cite:search0]]")
|
|
1474
|
+
- Use multiple citations when multiple sources support a point: .[[cite:search0]][[cite:search2]]
|
|
1475
|
+
- Cite sources even when paraphrasing
|
|
1476
|
+
|
|
1477
|
+
QUALITY CHECKS:
|
|
1478
|
+
✓ Have I synthesized from MULTIPLE sources (not just one)?
|
|
1479
|
+
✓ Have I written in my OWN words (not copied)?
|
|
1480
|
+
✓ Have I cited ALL factual claims?
|
|
1481
|
+
✓ Have I organized information thematically (not source-by-source)?
|
|
1482
|
+
✓ Have I avoided including word count targets in my output?
|
|
1483
|
+
|
|
1484
|
+
Write your research report now. Format in Markdown. Remember: NO word counts in section headings or anywhere in the output.
|
|
1485
|
+
"""
|
|
1486
|
+
|
|
1487
|
+
log.info("%s Calling LLM for report generation", log_identifier)
|
|
1488
|
+
|
|
1489
|
+
# Create LLM request with reasonable max tokens for faster generation
|
|
1490
|
+
# Reduced from 32000 to 8000 for better performance while still allowing comprehensive reports
|
|
1491
|
+
llm_request = LlmRequest(
|
|
1492
|
+
model=llm.model,
|
|
1493
|
+
contents=[adk_types.Content(role="user", parts=[adk_types.Part(text=report_prompt)])],
|
|
1494
|
+
config=adk_types.GenerateContentConfig(
|
|
1495
|
+
temperature=1.0,
|
|
1496
|
+
max_output_tokens=8000 # Reduced from 32000 for faster generation
|
|
1497
|
+
)
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
# Call LLM with streaming and progress updates
|
|
1501
|
+
report_body = ""
|
|
1502
|
+
response_count = 0
|
|
1503
|
+
last_progress_update = 0
|
|
1504
|
+
import time as time_module
|
|
1505
|
+
stream_start_time = time_module.time()
|
|
1506
|
+
|
|
1507
|
+
try:
|
|
1508
|
+
# IMPORTANT: Pass stream=True to enable streaming mode
|
|
1509
|
+
# Without this, the LLM call waits for the entire response before yielding,
|
|
1510
|
+
# which can cause timeouts with large prompts or slow models
|
|
1511
|
+
#
|
|
1512
|
+
# NOTE: LiteLlm streaming yields:
|
|
1513
|
+
# 1. Multiple partial responses (is_partial=True) with delta text chunks
|
|
1514
|
+
# 2. One final aggregated response (is_partial=False) with the FULL accumulated text
|
|
1515
|
+
#
|
|
1516
|
+
# We ONLY process partial responses to avoid duplication. The final aggregated
|
|
1517
|
+
# response contains the same text we've already accumulated from the partials.
|
|
1518
|
+
async for response_event in llm.generate_content_async(llm_request, stream=True):
|
|
1519
|
+
response_count += 1
|
|
1520
|
+
|
|
1521
|
+
# Check if this is a partial (streaming chunk) or final (aggregated) response
|
|
1522
|
+
# LiteLlm sets partial=True for streaming chunks, partial=False for final
|
|
1523
|
+
is_partial = getattr(response_event, 'partial', None)
|
|
1524
|
+
|
|
1525
|
+
# Skip non-partial (final aggregated) responses - they contain duplicate content
|
|
1526
|
+
# The final response has the full accumulated text which we've already collected
|
|
1527
|
+
if is_partial is False:
|
|
1528
|
+
continue
|
|
1529
|
+
|
|
1530
|
+
# Try different extraction methods
|
|
1531
|
+
extracted_text = ""
|
|
1532
|
+
if hasattr(response_event, 'text') and response_event.text:
|
|
1533
|
+
extracted_text = response_event.text
|
|
1534
|
+
elif hasattr(response_event, 'parts') and response_event.parts:
|
|
1535
|
+
extracted_text = "".join([part.text for part in response_event.parts if hasattr(part, 'text') and part.text])
|
|
1536
|
+
elif hasattr(response_event, 'content') and response_event.content:
|
|
1537
|
+
if hasattr(response_event.content, 'parts') and response_event.content.parts:
|
|
1538
|
+
extracted_text = "".join([part.text for part in response_event.content.parts if hasattr(part, 'text') and part.text])
|
|
1539
|
+
|
|
1540
|
+
if extracted_text:
|
|
1541
|
+
# For partial responses, always append (they are delta chunks)
|
|
1542
|
+
report_body += extracted_text
|
|
1543
|
+
|
|
1544
|
+
# Send progress update every 500 characters to show activity and reset peer timeout
|
|
1545
|
+
if len(report_body) - last_progress_update >= 500:
|
|
1546
|
+
last_progress_update = len(report_body)
|
|
1547
|
+
progress_pct = min(95, 85 + int((len(report_body) / 3000) * 10)) # 85-95%
|
|
1548
|
+
|
|
1549
|
+
# Send progress update to reset orchestrator's peer timeout
|
|
1550
|
+
await _send_research_progress(
|
|
1551
|
+
f"Writing report... ({len(report_body)} characters)",
|
|
1552
|
+
tool_context,
|
|
1553
|
+
phase="writing",
|
|
1554
|
+
progress_percentage=progress_pct,
|
|
1555
|
+
sources_found=len(all_findings)
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
log.info("%s Report generation complete: %d chars", log_identifier, len(report_body))
|
|
1559
|
+
|
|
1560
|
+
except Exception as stream_error:
|
|
1561
|
+
log.error("%s Error during LLM streaming: %s", log_identifier, str(stream_error))
|
|
1562
|
+
raise
|
|
1563
|
+
|
|
1564
|
+
# Add sources section
|
|
1565
|
+
sources_section = _generate_sources_section(all_findings)
|
|
1566
|
+
report_body += "\n\n" + sources_section
|
|
1567
|
+
|
|
1568
|
+
# Add methodology section
|
|
1569
|
+
methodology_section = _generate_methodology_section(all_findings)
|
|
1570
|
+
report_body += "\n\n" + methodology_section
|
|
1571
|
+
|
|
1572
|
+
return report_body
|
|
1573
|
+
|
|
1574
|
+
except Exception as e:
|
|
1575
|
+
log.error("%s LLM report generation failed: %s", log_identifier, str(e))
|
|
1576
|
+
return f"# Research Report: {research_question}\n\nError generating report: {str(e)}"
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
async def deep_research(
|
|
1580
|
+
research_question: str,
|
|
1581
|
+
research_type: str = "quick",
|
|
1582
|
+
sources: Optional[List[str]] = None,
|
|
1583
|
+
max_iterations: Optional[int] = None,
|
|
1584
|
+
max_sources_per_iteration: int = 5,
|
|
1585
|
+
kb_ids: Optional[List[str]] = None,
|
|
1586
|
+
max_runtime_minutes: Optional[int] = None,
|
|
1587
|
+
max_runtime_seconds: Optional[int] = None,
|
|
1588
|
+
tool_context: ToolContext = None,
|
|
1589
|
+
tool_config: Optional[Dict[str, Any]] = None,
|
|
1590
|
+
) -> Dict[str, Any]:
|
|
1591
|
+
"""
|
|
1592
|
+
Performs comprehensive, iterative research across multiple sources.
|
|
1593
|
+
|
|
1594
|
+
Configuration Priority (highest to lowest):
|
|
1595
|
+
1. Explicit parameters (max_iterations, max_runtime_minutes/max_runtime_seconds)
|
|
1596
|
+
2. Tool config (tool_config.max_iterations, tool_config.max_runtime_seconds)
|
|
1597
|
+
3. Research type translation ("quick" or "in-depth")
|
|
1598
|
+
|
|
1599
|
+
Args:
|
|
1600
|
+
research_question: The research question or topic to investigate
|
|
1601
|
+
research_type: Type of research - "quick" (5min, 3 iter) or "in-depth" (10min, 10 iter)
|
|
1602
|
+
sources: Sources to search (default from tool_config or ["web"])
|
|
1603
|
+
max_iterations: Maximum research iterations (overrides tool_config and research_type)
|
|
1604
|
+
max_sources_per_iteration: Max results per source per iteration (default: 5)
|
|
1605
|
+
kb_ids: Specific knowledge base IDs to search
|
|
1606
|
+
max_runtime_minutes: Maximum runtime in minutes (1-10). Converted to seconds internally.
|
|
1607
|
+
max_runtime_seconds: Maximum runtime in seconds (60-600). Overrides tool_config and research_type.
|
|
1608
|
+
tool_context: ADK tool context
|
|
1609
|
+
tool_config: Tool configuration with optional max_iterations, max_runtime_seconds, sources
|
|
1610
|
+
|
|
1611
|
+
Returns:
|
|
1612
|
+
Dictionary with research report and metadata
|
|
1613
|
+
"""
|
|
1614
|
+
log_identifier = "[DeepResearch]"
|
|
1615
|
+
log.info("%s Starting deep research: %s", log_identifier, research_question)
|
|
1616
|
+
|
|
1617
|
+
# Resolve configuration with priority: explicit params > tool_config > research_type
|
|
1618
|
+
config = tool_config or {}
|
|
1619
|
+
|
|
1620
|
+
# Resolve max_iterations
|
|
1621
|
+
if max_iterations is None:
|
|
1622
|
+
max_iterations = config.get("max_iterations")
|
|
1623
|
+
if max_iterations is not None:
|
|
1624
|
+
log.info("%s Using max_iterations from tool_config: %d", log_identifier, max_iterations)
|
|
1625
|
+
else:
|
|
1626
|
+
# Fallback to research_type translation
|
|
1627
|
+
if research_type.lower() in ["in-depth", "indepth", "in_depth", "deep", "comprehensive"]:
|
|
1628
|
+
max_iterations = 10
|
|
1629
|
+
log.info("%s Using max_iterations from research_type 'in-depth': %d", log_identifier, max_iterations)
|
|
1630
|
+
else:
|
|
1631
|
+
max_iterations = 3
|
|
1632
|
+
log.info("%s Using max_iterations from research_type 'quick': %d", log_identifier, max_iterations)
|
|
1633
|
+
else:
|
|
1634
|
+
log.info("%s Using explicit max_iterations parameter: %d", log_identifier, max_iterations)
|
|
1635
|
+
|
|
1636
|
+
# Resolve max_runtime_seconds (with priority: max_runtime_minutes > max_runtime_seconds > tool_config > research_type)
|
|
1637
|
+
# First, check if max_runtime_minutes was provided (LLM-friendly parameter)
|
|
1638
|
+
if max_runtime_minutes is not None:
|
|
1639
|
+
max_runtime_seconds = max_runtime_minutes * 60
|
|
1640
|
+
log.info("%s Using explicit max_runtime_minutes parameter: %d minutes (%d seconds)",
|
|
1641
|
+
log_identifier, max_runtime_minutes, max_runtime_seconds)
|
|
1642
|
+
elif max_runtime_seconds is not None:
|
|
1643
|
+
log.info("%s Using explicit max_runtime_seconds parameter: %d", log_identifier, max_runtime_seconds)
|
|
1644
|
+
else:
|
|
1645
|
+
# Check tool_config (support both seconds and minutes)
|
|
1646
|
+
config_duration = config.get("max_runtime_seconds") or config.get("duration_seconds")
|
|
1647
|
+
config_duration_minutes = config.get("duration_minutes")
|
|
1648
|
+
|
|
1649
|
+
if config_duration is not None:
|
|
1650
|
+
max_runtime_seconds = config_duration
|
|
1651
|
+
log.info("%s Using max_runtime_seconds from tool_config: %d", log_identifier, max_runtime_seconds)
|
|
1652
|
+
elif config_duration_minutes is not None:
|
|
1653
|
+
max_runtime_seconds = config_duration_minutes * 60
|
|
1654
|
+
log.info("%s Using duration_minutes from tool_config: %d minutes (%d seconds)",
|
|
1655
|
+
log_identifier, config_duration_minutes, max_runtime_seconds)
|
|
1656
|
+
else:
|
|
1657
|
+
# Fallback to research_type translation
|
|
1658
|
+
if research_type.lower() in ["in-depth", "indepth", "in_depth", "deep", "comprehensive"]:
|
|
1659
|
+
max_runtime_seconds = 600 # 10 minutes
|
|
1660
|
+
log.info("%s Using max_runtime_seconds from research_type 'in-depth': %d seconds",
|
|
1661
|
+
log_identifier, max_runtime_seconds)
|
|
1662
|
+
else:
|
|
1663
|
+
max_runtime_seconds = 300 # 5 minutes
|
|
1664
|
+
log.info("%s Using max_runtime_seconds from research_type 'quick': %d seconds",
|
|
1665
|
+
log_identifier, max_runtime_seconds)
|
|
1666
|
+
|
|
1667
|
+
# Resolve sources
|
|
1668
|
+
if sources is None:
|
|
1669
|
+
sources = config.get("sources", ["web"])
|
|
1670
|
+
log.info("%s Using sources from config: %s", log_identifier, sources)
|
|
1671
|
+
|
|
1672
|
+
if not tool_context:
|
|
1673
|
+
return {"status": "error", "message": "ToolContext is missing"}
|
|
1674
|
+
|
|
1675
|
+
# Default sources - web only
|
|
1676
|
+
if sources is None:
|
|
1677
|
+
sources = ["web"]
|
|
1678
|
+
|
|
1679
|
+
# Track start time for runtime limit
|
|
1680
|
+
import time
|
|
1681
|
+
start_time = time.time()
|
|
1682
|
+
|
|
1683
|
+
# Validate and filter sources
|
|
1684
|
+
if sources:
|
|
1685
|
+
# Validate and filter sources - only allow web and kb
|
|
1686
|
+
allowed_sources = {"web", "kb"}
|
|
1687
|
+
sources = [s for s in sources if s in allowed_sources]
|
|
1688
|
+
|
|
1689
|
+
# If no valid sources after filtering, use default
|
|
1690
|
+
if not sources:
|
|
1691
|
+
log.warning("%s No valid sources provided, using default: ['web']", log_identifier)
|
|
1692
|
+
sources = ["web"]
|
|
1693
|
+
else:
|
|
1694
|
+
log.info("%s Using validated sources: %s", log_identifier, sources)
|
|
1695
|
+
|
|
1696
|
+
# Validate iterations and runtime
|
|
1697
|
+
max_iterations = max(1, min(max_iterations, 10))
|
|
1698
|
+
if max_runtime_seconds:
|
|
1699
|
+
max_runtime_seconds = max(60, min(max_runtime_seconds, 600)) # 1-10 minutes
|
|
1700
|
+
log.info("%s Runtime limit set to %d seconds", log_identifier, max_runtime_seconds)
|
|
1701
|
+
|
|
1702
|
+
try:
|
|
1703
|
+
# Initialize citation tracker
|
|
1704
|
+
citation_tracker = ResearchCitationTracker(research_question)
|
|
1705
|
+
|
|
1706
|
+
# Send initial progress with structured data
|
|
1707
|
+
await _send_research_progress(
|
|
1708
|
+
"Planning research strategy and generating search queries...",
|
|
1709
|
+
tool_context,
|
|
1710
|
+
phase="planning",
|
|
1711
|
+
progress_percentage=5,
|
|
1712
|
+
current_iteration=0,
|
|
1713
|
+
total_iterations=max_iterations,
|
|
1714
|
+
sources_found=0,
|
|
1715
|
+
elapsed_seconds=int(time.time() - start_time),
|
|
1716
|
+
max_runtime_seconds=max_runtime_seconds or 0
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1719
|
+
# Generate initial queries using LLM (with phase-specific model support)
|
|
1720
|
+
queries = await _generate_initial_queries(research_question, tool_context, tool_config)
|
|
1721
|
+
log.info("%s Generated %d initial queries", log_identifier, len(queries))
|
|
1722
|
+
|
|
1723
|
+
# Generate human-readable title for the research using LLM
|
|
1724
|
+
log.info("%s Generating LLM title for research question: %s", log_identifier, research_question[:100])
|
|
1725
|
+
research_title = await _generate_research_title(research_question, tool_context, tool_config)
|
|
1726
|
+
citation_tracker.set_title(research_title)
|
|
1727
|
+
log.info("%s LLM-generated research title: '%s' (original query: '%s')",
|
|
1728
|
+
log_identifier, research_title, research_question[:50])
|
|
1729
|
+
|
|
1730
|
+
# Send initial RAG info update with title (no sources yet)
|
|
1731
|
+
# This allows the UI to display the title in the RAG info panel immediately
|
|
1732
|
+
await _send_rag_info_update(citation_tracker, tool_context, is_complete=False)
|
|
1733
|
+
|
|
1734
|
+
# Iterative research loop
|
|
1735
|
+
all_findings: List[SearchResult] = []
|
|
1736
|
+
seen_sources_global = set() # Track seen sources across ALL iterations
|
|
1737
|
+
|
|
1738
|
+
for iteration in range(1, max_iterations + 1):
|
|
1739
|
+
# Check runtime limit - only applies to research iterations, not report generation
|
|
1740
|
+
if max_runtime_seconds:
|
|
1741
|
+
elapsed = time.time() - start_time
|
|
1742
|
+
if elapsed >= max_runtime_seconds:
|
|
1743
|
+
log.info("%s Runtime limit reached (%d seconds), stopping research iterations. Will proceed to generate report from %d sources.",
|
|
1744
|
+
log_identifier, max_runtime_seconds, len(all_findings))
|
|
1745
|
+
await _send_research_progress(
|
|
1746
|
+
f"Research time limit reached ({int(elapsed)}s). Proceeding to generate report from {len(all_findings)} sources...",
|
|
1747
|
+
tool_context,
|
|
1748
|
+
phase="writing",
|
|
1749
|
+
progress_percentage=80,
|
|
1750
|
+
current_iteration=iteration,
|
|
1751
|
+
total_iterations=max_iterations,
|
|
1752
|
+
sources_found=len(all_findings),
|
|
1753
|
+
elapsed_seconds=int(elapsed),
|
|
1754
|
+
max_runtime_seconds=max_runtime_seconds
|
|
1755
|
+
)
|
|
1756
|
+
break
|
|
1757
|
+
|
|
1758
|
+
log.info("%s === Iteration %d/%d ===", log_identifier, iteration, max_iterations)
|
|
1759
|
+
|
|
1760
|
+
# Calculate progress percentage for this iteration
|
|
1761
|
+
iteration_progress_base = 10 + ((iteration - 1) / max_iterations) * 70 # 10-80% for iterations
|
|
1762
|
+
|
|
1763
|
+
# Search with current queries
|
|
1764
|
+
iteration_findings = []
|
|
1765
|
+
for query_idx, query in enumerate(queries, 1):
|
|
1766
|
+
# Start tracking this query in citation tracker
|
|
1767
|
+
citation_tracker.start_query(query)
|
|
1768
|
+
|
|
1769
|
+
# Calculate sub-progress within iteration
|
|
1770
|
+
query_progress = iteration_progress_base + (query_idx / len(queries)) * (70 / max_iterations) * 0.3
|
|
1771
|
+
|
|
1772
|
+
# Send progress for each query with structured data
|
|
1773
|
+
await _send_research_progress(
|
|
1774
|
+
f"{query[:60]}...",
|
|
1775
|
+
tool_context,
|
|
1776
|
+
phase="searching",
|
|
1777
|
+
progress_percentage=int(query_progress),
|
|
1778
|
+
current_iteration=iteration,
|
|
1779
|
+
total_iterations=max_iterations,
|
|
1780
|
+
sources_found=len(all_findings),
|
|
1781
|
+
current_query=query,
|
|
1782
|
+
elapsed_seconds=int(time.time() - start_time),
|
|
1783
|
+
max_runtime_seconds=max_runtime_seconds or 0
|
|
1784
|
+
)
|
|
1785
|
+
results = await _multi_source_search(
|
|
1786
|
+
query, sources, max_sources_per_iteration,
|
|
1787
|
+
kb_ids, tool_context, tool_config
|
|
1788
|
+
)
|
|
1789
|
+
|
|
1790
|
+
# Deduplicate against ALL previously seen sources (web-only version)
|
|
1791
|
+
query_findings = []
|
|
1792
|
+
for result in results:
|
|
1793
|
+
# For web sources, use URL or title as unique key
|
|
1794
|
+
key = result.url or f"web:{result.title}"
|
|
1795
|
+
|
|
1796
|
+
# Only add if not seen before
|
|
1797
|
+
if key not in seen_sources_global:
|
|
1798
|
+
seen_sources_global.add(key)
|
|
1799
|
+
query_findings.append(result)
|
|
1800
|
+
iteration_findings.append(result)
|
|
1801
|
+
|
|
1802
|
+
# Add citations for this query's findings
|
|
1803
|
+
for finding in query_findings:
|
|
1804
|
+
citation_tracker.add_citation(finding, query)
|
|
1805
|
+
|
|
1806
|
+
all_findings.extend(iteration_findings)
|
|
1807
|
+
|
|
1808
|
+
log.info("%s Iteration %d found %d new sources (total: %d)",
|
|
1809
|
+
log_identifier, iteration, len(iteration_findings), len(all_findings))
|
|
1810
|
+
|
|
1811
|
+
# Send RAG info update with new sources after each iteration
|
|
1812
|
+
# This allows the UI to display sources as they are discovered
|
|
1813
|
+
if iteration_findings:
|
|
1814
|
+
await _send_rag_info_update(citation_tracker, tool_context, is_complete=False)
|
|
1815
|
+
|
|
1816
|
+
# Select and fetch full content from best sources in THIS iteration
|
|
1817
|
+
# This allows the LLM to reflect on full content, not just snippets
|
|
1818
|
+
selection_progress = iteration_progress_base + (70 / max_iterations) * 0.4
|
|
1819
|
+
|
|
1820
|
+
# Prepare URL list early for the entire analyzing phase
|
|
1821
|
+
fetching_url_list = []
|
|
1822
|
+
|
|
1823
|
+
# Select top 2-3 sources from this iteration to fetch/analyze
|
|
1824
|
+
sources_to_display_count = min(3, len(all_findings))
|
|
1825
|
+
|
|
1826
|
+
# For web sources: select and fetch full content from current iteration (with phase-specific model support)
|
|
1827
|
+
selected_sources = []
|
|
1828
|
+
if len(iteration_findings) > 0:
|
|
1829
|
+
sources_to_fetch_count = min(3, len(iteration_findings))
|
|
1830
|
+
selected_sources = await _select_sources_to_fetch(
|
|
1831
|
+
research_question, iteration_findings, max_to_fetch=sources_to_fetch_count,
|
|
1832
|
+
tool_context=tool_context, tool_config=tool_config
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
# Prepare display list for UI - show ONLY NEW sources being analyzed (not duplicates)
|
|
1836
|
+
# Use iteration_findings which contains only NEW sources after deduplication
|
|
1837
|
+
if selected_sources:
|
|
1838
|
+
# Web sources that will be fetched (only new ones)
|
|
1839
|
+
fetching_url_list = [
|
|
1840
|
+
{
|
|
1841
|
+
"url": src.url,
|
|
1842
|
+
"title": src.title,
|
|
1843
|
+
"favicon": f"https://www.google.com/s2/favicons?domain={src.url}&sz=32" if src.url else "",
|
|
1844
|
+
"source_type": src.source_type
|
|
1845
|
+
}
|
|
1846
|
+
for src in selected_sources
|
|
1847
|
+
]
|
|
1848
|
+
else:
|
|
1849
|
+
# Web-only version - no other sources to display
|
|
1850
|
+
fetching_url_list = []
|
|
1851
|
+
|
|
1852
|
+
# Start unified "analyzing" phase - covers selecting, fetching, and analyzing
|
|
1853
|
+
# Skip if no sources found
|
|
1854
|
+
if len(all_findings) > 0:
|
|
1855
|
+
analyze_progress = iteration_progress_base + (70 / max_iterations) * 0.4
|
|
1856
|
+
await _send_research_progress(
|
|
1857
|
+
f"Analyzing {len(all_findings)} sources (reading {len(fetching_url_list)} in detail)...",
|
|
1858
|
+
tool_context,
|
|
1859
|
+
phase="analyzing",
|
|
1860
|
+
progress_percentage=int(analyze_progress),
|
|
1861
|
+
current_iteration=iteration,
|
|
1862
|
+
total_iterations=max_iterations,
|
|
1863
|
+
sources_found=len(all_findings),
|
|
1864
|
+
fetching_urls=fetching_url_list,
|
|
1865
|
+
elapsed_seconds=int(time.time() - start_time),
|
|
1866
|
+
max_runtime_seconds=max_runtime_seconds or 0
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
# Fetch selected sources (still within analyzing phase) - only for web sources
|
|
1870
|
+
if selected_sources:
|
|
1871
|
+
fetch_stats = await _fetch_selected_sources(selected_sources, tool_context, tool_config, citation_tracker, start_time, max_runtime_seconds)
|
|
1872
|
+
log.info("%s Iteration %d fetch stats: %s", log_identifier, iteration, fetch_stats)
|
|
1873
|
+
|
|
1874
|
+
# Continue analyzing phase - reflect on findings
|
|
1875
|
+
# Skip if no sources found
|
|
1876
|
+
if len(all_findings) > 0:
|
|
1877
|
+
reflect_progress = iteration_progress_base + (70 / max_iterations) * 0.9
|
|
1878
|
+
await _send_research_progress(
|
|
1879
|
+
f"Analyzing {len(all_findings)} sources and identifying knowledge gaps...",
|
|
1880
|
+
tool_context,
|
|
1881
|
+
phase="analyzing",
|
|
1882
|
+
progress_percentage=int(reflect_progress),
|
|
1883
|
+
current_iteration=iteration,
|
|
1884
|
+
total_iterations=max_iterations,
|
|
1885
|
+
sources_found=len(all_findings),
|
|
1886
|
+
fetching_urls=fetching_url_list, # Keep URLs visible during reflection
|
|
1887
|
+
elapsed_seconds=int(time.time() - start_time),
|
|
1888
|
+
max_runtime_seconds=max_runtime_seconds or 0
|
|
1889
|
+
)
|
|
1890
|
+
|
|
1891
|
+
reflection = await _reflect_on_findings(
|
|
1892
|
+
research_question, all_findings, iteration, tool_context, max_iterations, tool_config
|
|
1893
|
+
)
|
|
1894
|
+
|
|
1895
|
+
log.info("%s Reflection: %s", log_identifier, reflection.reasoning)
|
|
1896
|
+
|
|
1897
|
+
# Check if we should continue
|
|
1898
|
+
if not reflection.should_continue or iteration >= max_iterations:
|
|
1899
|
+
log.info("%s Research complete after %d iterations", log_identifier, iteration)
|
|
1900
|
+
break
|
|
1901
|
+
|
|
1902
|
+
# Generate new queries for next iteration based on reflection
|
|
1903
|
+
queries = reflection.suggested_queries
|
|
1904
|
+
|
|
1905
|
+
# Generate final report
|
|
1906
|
+
await _send_research_progress(
|
|
1907
|
+
f"Writing comprehensive research report from {len(all_findings)} sources...",
|
|
1908
|
+
tool_context,
|
|
1909
|
+
phase="writing",
|
|
1910
|
+
progress_percentage=85,
|
|
1911
|
+
current_iteration=max_iterations,
|
|
1912
|
+
total_iterations=max_iterations,
|
|
1913
|
+
sources_found=len(all_findings),
|
|
1914
|
+
elapsed_seconds=int(time.time() - start_time),
|
|
1915
|
+
max_runtime_seconds=max_runtime_seconds or 0
|
|
1916
|
+
)
|
|
1917
|
+
|
|
1918
|
+
report = await _generate_research_report(
|
|
1919
|
+
research_question, all_findings, citation_tracker, tool_context, tool_config
|
|
1920
|
+
)
|
|
1921
|
+
|
|
1922
|
+
log.info("%s Research complete: %d total sources, report length: %d chars",
|
|
1923
|
+
log_identifier, len(all_findings), len(report))
|
|
1924
|
+
|
|
1925
|
+
from ..utils.artifact_helpers import (
|
|
1926
|
+
save_artifact_with_metadata,
|
|
1927
|
+
decode_and_get_bytes,
|
|
1928
|
+
sanitize_to_filename,
|
|
1929
|
+
)
|
|
1930
|
+
from ..utils.context_helpers import get_original_session_id
|
|
1931
|
+
|
|
1932
|
+
# Generate filename from research question using utility function
|
|
1933
|
+
artifact_filename = sanitize_to_filename(
|
|
1934
|
+
research_question,
|
|
1935
|
+
max_length=50,
|
|
1936
|
+
suffix="_report.md"
|
|
1937
|
+
)
|
|
1938
|
+
# Get artifact service from invocation context
|
|
1939
|
+
inv_context = tool_context._invocation_context
|
|
1940
|
+
artifact_service = inv_context.artifact_service
|
|
1941
|
+
if not artifact_service:
|
|
1942
|
+
log.warning("%s ArtifactService not available, cannot save research report artifact", log_identifier)
|
|
1943
|
+
artifact_result = {"status": "error", "message": "ArtifactService not available"}
|
|
1944
|
+
else:
|
|
1945
|
+
# Prepare content bytes and metadata
|
|
1946
|
+
try:
|
|
1947
|
+
artifact_bytes, final_mime_type = decode_and_get_bytes(
|
|
1948
|
+
report, "text/markdown", f"{log_identifier}[CreateArtifact]"
|
|
1949
|
+
)
|
|
1950
|
+
except Exception as decode_error:
|
|
1951
|
+
log.error("%s Error preparing artifact bytes: %s", log_identifier, str(decode_error))
|
|
1952
|
+
raise
|
|
1953
|
+
|
|
1954
|
+
# Get timestamp from session
|
|
1955
|
+
session_last_update_time = inv_context.session.last_update_time
|
|
1956
|
+
if isinstance(session_last_update_time, datetime):
|
|
1957
|
+
timestamp_for_artifact = session_last_update_time
|
|
1958
|
+
elif isinstance(session_last_update_time, (int, float)):
|
|
1959
|
+
try:
|
|
1960
|
+
timestamp_for_artifact = datetime.fromtimestamp(session_last_update_time, timezone.utc)
|
|
1961
|
+
except Exception:
|
|
1962
|
+
timestamp_for_artifact = datetime.now(timezone.utc)
|
|
1963
|
+
else:
|
|
1964
|
+
timestamp_for_artifact = datetime.now(timezone.utc)
|
|
1965
|
+
|
|
1966
|
+
# Save artifact directly using artifact service
|
|
1967
|
+
try:
|
|
1968
|
+
artifact_result = await save_artifact_with_metadata(
|
|
1969
|
+
artifact_service=artifact_service,
|
|
1970
|
+
app_name=inv_context.app_name,
|
|
1971
|
+
user_id=inv_context.user_id,
|
|
1972
|
+
session_id=get_original_session_id(inv_context),
|
|
1973
|
+
filename=artifact_filename,
|
|
1974
|
+
content_bytes=artifact_bytes,
|
|
1975
|
+
mime_type=final_mime_type,
|
|
1976
|
+
metadata_dict={"description": f"Deep research report on: {research_question}"},
|
|
1977
|
+
timestamp=timestamp_for_artifact,
|
|
1978
|
+
schema_max_keys=50, # Default schema max keys
|
|
1979
|
+
tool_context=tool_context,
|
|
1980
|
+
)
|
|
1981
|
+
except Exception as save_error:
|
|
1982
|
+
log.error("%s Error saving artifact: %s", log_identifier, str(save_error))
|
|
1983
|
+
artifact_result = {"status": "error", "message": str(save_error)}
|
|
1984
|
+
|
|
1985
|
+
if artifact_result.get("status") not in ["success", "partial_success"]:
|
|
1986
|
+
log.error("%s Failed to create artifact for research report. Status: %s, Message: %s",
|
|
1987
|
+
log_identifier, artifact_result.get("status"), artifact_result.get("message"))
|
|
1988
|
+
artifact_version = None
|
|
1989
|
+
else:
|
|
1990
|
+
artifact_version = artifact_result.get("data_version", 1)
|
|
1991
|
+
log.info("%s Successfully created artifact '%s' v%d",
|
|
1992
|
+
log_identifier, artifact_filename, artifact_version)
|
|
1993
|
+
|
|
1994
|
+
# Send final progress update
|
|
1995
|
+
try:
|
|
1996
|
+
await _send_research_progress(
|
|
1997
|
+
f"✅ Research complete! Report saved as '{artifact_filename}'",
|
|
1998
|
+
tool_context,
|
|
1999
|
+
phase="writing",
|
|
2000
|
+
progress_percentage=100,
|
|
2001
|
+
current_iteration=max_iterations,
|
|
2002
|
+
total_iterations=max_iterations,
|
|
2003
|
+
sources_found=len(all_findings),
|
|
2004
|
+
elapsed_seconds=int(time.time() - start_time),
|
|
2005
|
+
max_runtime_seconds=max_runtime_seconds or 0
|
|
2006
|
+
)
|
|
2007
|
+
except Exception as progress_error:
|
|
2008
|
+
log.error("%s Error sending final progress update: %s", log_identifier, str(progress_error))
|
|
2009
|
+
|
|
2010
|
+
# Emit DeepResearchReportData signal directly to frontend
|
|
2011
|
+
# This bypasses the LLM response entirely, ensuring the report is displayed
|
|
2012
|
+
# via the DeepResearchReportBubble component
|
|
2013
|
+
try:
|
|
2014
|
+
await _send_deep_research_report_signal(
|
|
2015
|
+
artifact_filename=artifact_filename,
|
|
2016
|
+
artifact_version=artifact_version,
|
|
2017
|
+
title=citation_tracker.generated_title or research_question,
|
|
2018
|
+
sources_count=len(all_findings),
|
|
2019
|
+
tool_context=tool_context
|
|
2020
|
+
)
|
|
2021
|
+
|
|
2022
|
+
except Exception as signal_error:
|
|
2023
|
+
log.error("%s Error sending deep research report signal: %s", log_identifier, str(signal_error))
|
|
2024
|
+
|
|
2025
|
+
# Send final RAG info update marking research as complete
|
|
2026
|
+
try:
|
|
2027
|
+
await _send_rag_info_update(citation_tracker, tool_context, is_complete=True)
|
|
2028
|
+
except Exception as rag_error:
|
|
2029
|
+
log.error("%s Error sending final RAG info update: %s", log_identifier, str(rag_error))
|
|
2030
|
+
|
|
2031
|
+
# Build the response - NO EMBED since we already sent the DeepResearchReportData signal
|
|
2032
|
+
artifact_save_success = artifact_result.get("status") in ["success", "partial_success"]
|
|
2033
|
+
artifact_version = artifact_result.get("data_version", 1) if artifact_save_success else None
|
|
2034
|
+
|
|
2035
|
+
result_dict = {
|
|
2036
|
+
"status": "success",
|
|
2037
|
+
"total_sources": len(all_findings),
|
|
2038
|
+
"iterations_completed": min(iteration, max_iterations),
|
|
2039
|
+
"rag_metadata": citation_tracker.get_rag_metadata(artifact_filename=artifact_filename if artifact_save_success else None),
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
# Only include artifact info if save was successful
|
|
2043
|
+
if artifact_save_success:
|
|
2044
|
+
result_dict["artifact_filename"] = artifact_filename
|
|
2045
|
+
result_dict["artifact_version"] = artifact_version
|
|
2046
|
+
result_dict["response_artifact"] = {
|
|
2047
|
+
"filename": artifact_filename,
|
|
2048
|
+
"version": artifact_version
|
|
2049
|
+
}
|
|
2050
|
+
result_dict["message"] = (
|
|
2051
|
+
f"Deep research complete: analyzed {len(all_findings)} sources. "
|
|
2052
|
+
f"The comprehensive report '{artifact_filename}' (version {artifact_version}) "
|
|
2053
|
+
f"has been sent to the user and will be displayed automatically. "
|
|
2054
|
+
f"Do NOT include any artifact embeds or summarize the report - it is already being displayed."
|
|
2055
|
+
)
|
|
2056
|
+
else:
|
|
2057
|
+
result_dict["artifact_error"] = artifact_result.get("message", "Failed to save artifact")
|
|
2058
|
+
result_dict["message"] = f"Research complete but failed to save artifact: {artifact_result.get('message', 'Unknown error')}. Analyzed {len(all_findings)} sources."
|
|
2059
|
+
|
|
2060
|
+
return result_dict
|
|
2061
|
+
|
|
2062
|
+
except Exception as e:
|
|
2063
|
+
log.exception("%s Unexpected error: %s", log_identifier, e)
|
|
2064
|
+
return {
|
|
2065
|
+
"status": "error",
|
|
2066
|
+
"message": f"Research failed: {str(e)}"
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
|
|
2070
|
+
# Tool Definition
|
|
2071
|
+
deep_research_tool_def = BuiltinTool(
|
|
2072
|
+
name="deep_research",
|
|
2073
|
+
implementation=deep_research,
|
|
2074
|
+
description="""
|
|
2075
|
+
Performs comprehensive, iterative research across multiple sources.
|
|
2076
|
+
|
|
2077
|
+
This tool conducts deep research by:
|
|
2078
|
+
1. Breaking down the research question into searchable queries
|
|
2079
|
+
2. Searching across web and knowledge base sources
|
|
2080
|
+
3. Reflecting on findings to identify gaps
|
|
2081
|
+
4. Refining queries and conducting additional searches
|
|
2082
|
+
5. Synthesizing findings into a comprehensive report with citations
|
|
2083
|
+
|
|
2084
|
+
Use this tool when you need to:
|
|
2085
|
+
- Gather comprehensive information on a complex topic
|
|
2086
|
+
- Research across multiple information sources
|
|
2087
|
+
- Provide well-cited, authoritative answers
|
|
2088
|
+
- Explore a topic in depth with multiple perspectives
|
|
2089
|
+
|
|
2090
|
+
The tool provides real-time progress updates and generates a detailed
|
|
2091
|
+
research report with proper citations for all sources.
|
|
2092
|
+
|
|
2093
|
+
IMPORTANT - Returning Results:
|
|
2094
|
+
The tool automatically sends the research report directly to the user's interface
|
|
2095
|
+
via a special signal. The report will be displayed automatically in a dedicated
|
|
2096
|
+
component. Do NOT include any artifact embeds or summarize the report content -
|
|
2097
|
+
it is already being displayed to the user. Simply acknowledge that the research
|
|
2098
|
+
is complete and the report has been delivered.
|
|
2099
|
+
|
|
2100
|
+
Configuration:
|
|
2101
|
+
- Can be configured via tool_config in agent YAML (max_iterations, max_runtime_seconds, sources)
|
|
2102
|
+
- Can be overridden via explicit parameters
|
|
2103
|
+
- Supports research_type for backward compatibility ("quick" or "in-depth")
|
|
2104
|
+
""",
|
|
2105
|
+
category="research",
|
|
2106
|
+
category_name=CATEGORY_NAME,
|
|
2107
|
+
category_description=CATEGORY_DESCRIPTION,
|
|
2108
|
+
required_scopes=["tool:research:deep_research"],
|
|
2109
|
+
parameters=adk_types.Schema(
|
|
2110
|
+
type=adk_types.Type.OBJECT,
|
|
2111
|
+
properties={
|
|
2112
|
+
"research_question": adk_types.Schema(
|
|
2113
|
+
type=adk_types.Type.STRING,
|
|
2114
|
+
description="The research question or topic to investigate"
|
|
2115
|
+
),
|
|
2116
|
+
"research_type": adk_types.Schema(
|
|
2117
|
+
type=adk_types.Type.STRING,
|
|
2118
|
+
description="Type of research: 'quick' (5min, 3 iterations) or 'in-depth' (10min, 10 iterations). Can be overridden by tool_config or explicit parameters. Default: 'quick'",
|
|
2119
|
+
enum=["quick", "in-depth"],
|
|
2120
|
+
nullable=True
|
|
2121
|
+
),
|
|
2122
|
+
"max_iterations": adk_types.Schema(
|
|
2123
|
+
type=adk_types.Type.INTEGER,
|
|
2124
|
+
description="Maximum number of research iterations (1-10). Overrides tool_config and research_type if provided.",
|
|
2125
|
+
nullable=True
|
|
2126
|
+
),
|
|
2127
|
+
"max_runtime_minutes": adk_types.Schema(
|
|
2128
|
+
type=adk_types.Type.INTEGER,
|
|
2129
|
+
description="Maximum runtime in minutes (1-10). The software converts this to seconds internally. Overrides tool_config and research_type if provided.",
|
|
2130
|
+
nullable=True
|
|
2131
|
+
),
|
|
2132
|
+
"max_runtime_seconds": adk_types.Schema(
|
|
2133
|
+
type=adk_types.Type.INTEGER,
|
|
2134
|
+
description="Maximum runtime in seconds (60-600). Use max_runtime_minutes instead for easier specification. Overrides tool_config and research_type if provided.",
|
|
2135
|
+
nullable=True
|
|
2136
|
+
),
|
|
2137
|
+
"sources": adk_types.Schema(
|
|
2138
|
+
type=adk_types.Type.ARRAY,
|
|
2139
|
+
items=adk_types.Schema(
|
|
2140
|
+
type=adk_types.Type.STRING,
|
|
2141
|
+
enum=["web", "kb"]
|
|
2142
|
+
),
|
|
2143
|
+
description="Sources to search. Default from tool_config or ['web']. Web search requires Google Custom Search API key (GOOGLE_API_KEY and GOOGLE_CSE_ID). For other search providers (Tavily, Exa, Brave), use the corresponding plugins from solace-agent-mesh-plugins.",
|
|
2144
|
+
nullable=True
|
|
2145
|
+
),
|
|
2146
|
+
"kb_ids": adk_types.Schema(
|
|
2147
|
+
type=adk_types.Type.ARRAY,
|
|
2148
|
+
items=adk_types.Schema(type=adk_types.Type.STRING),
|
|
2149
|
+
description="Specific knowledge base IDs to search (only used if 'kb' is in sources)",
|
|
2150
|
+
nullable=True
|
|
2151
|
+
)
|
|
2152
|
+
},
|
|
2153
|
+
required=["research_question"]
|
|
2154
|
+
),
|
|
2155
|
+
examples=[]
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
# Register tool
|
|
2159
|
+
tool_registry.register(deep_research_tool_def)
|
|
2160
|
+
|
|
2161
|
+
log.info("Deep research tool registered: deep_research")
|