sunholo 0.145.0__tar.gz → 0.145.1__tar.gz
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.
- {sunholo-0.145.0/src/sunholo.egg-info → sunholo-0.145.1}/PKG-INFO +2 -2
- {sunholo-0.145.0 → sunholo-0.145.1}/pyproject.toml +3 -3
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/invoke/__init__.py +1 -1
- {sunholo-0.145.0 → sunholo-0.145.1/src/sunholo.egg-info}/PKG-INFO +2 -2
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo.egg-info/SOURCES.txt +2 -3
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo.egg-info/requires.txt +1 -0
- sunholo-0.145.1/tests/test_async_task_runner.py +355 -0
- sunholo-0.145.0/tests/test_vac_routes_fastapi.py +0 -226
- sunholo-0.145.0/tests/test_vac_routes_mcp.py +0 -398
- {sunholo-0.145.0 → sunholo-0.145.1}/LICENSE.txt +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/MANIFEST.in +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/README.md +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/setup.cfg +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/a2a/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/a2a/agent_card.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/a2a/task_manager.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/a2a/vac_a2a_agent.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/chat_history.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/dispatch_to_qa.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/fastapi/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/fastapi/base.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/fastapi/qna_routes.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/fastapi/vac_routes.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/flask/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/flask/base.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/flask/vac_routes.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/langserve.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/pubsub.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/route.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/special_commands.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/agents/swagger.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/archive/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/archive/archive.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/auth/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/auth/gcloud.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/auth/refresh.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/auth/run.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/azure/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/azure/auth.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/azure/blobs.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/azure/event_grid.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/bots/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/bots/discord.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/bots/github_webhook.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/bots/webapp.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/azure.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/doc_handling.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/encode_metadata.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/images.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/loaders.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/message_data.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/pdfs.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/process_chunker_data.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/publish.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/pubsub.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/chunker/splitter.py +0 -0
- {sunholo-0.145.0/src/sunholo/templates/system_services → sunholo-0.145.1/src/sunholo/cli}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/chat_vac.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/cli.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/cli_init.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/configs.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/deploy.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/embedder.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/merge_texts.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/run_proxy.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/sun_rich.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/swagger.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/cli/vertex.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/components/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/components/llm.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/components/retriever.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/components/vectorstore.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/custom_logging.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/alloydb.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/alloydb_client.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/database.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/lancedb.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/sql/sb/create_function.sql +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/sql/sb/create_function_time.sql +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/sql/sb/create_table.sql +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/sql/sb/delete_source_row.sql +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/sql/sb/return_sources.sql +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/sql/sb/setup.sql +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/static_dbs.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/database/uuid.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/discovery_engine/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/discovery_engine/chunker_handler.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/discovery_engine/cli.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/discovery_engine/create_new.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/discovery_engine/discovery_engine_client.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/embedder/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/embedder/embed_chunk.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/embedder/embed_metadata.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/excel/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/excel/plugin.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/add_file.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/download_folder.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/download_gcs_text.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/download_url.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/extract_and_sign.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/gcs/metadata.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/file_handling.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/genaiv2.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/images.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/init.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/process_funcs_cls.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/genai/safety.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/invoke/async_class.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/invoke/async_task_runner.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/invoke/direct_vac_func.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/invoke/invoke_vac_utils.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/langchain_types.py +0 -0
- {sunholo-0.145.0/src/sunholo/templates/project → sunholo-0.145.1/src/sunholo/langfuse}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/langfuse/callback.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/langfuse/evals.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/langfuse/prompts.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/llamaindex/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/llamaindex/get_files.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/llamaindex/import_files.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/llamaindex/llamaindex_class.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/llamaindex/user_history.py +0 -0
- {sunholo-0.145.0/src/sunholo/templates/agent/tools → sunholo-0.145.1/src/sunholo/lookup}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/lookup/model_lookup.yaml +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/cli.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/cli_fastmcp.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/extensible_mcp_server.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/mcp_manager.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/sse_utils.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/stdio_http_bridge.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/vac_mcp_server.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/vac_mcp_server_fastmcp.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/mcp/vac_tools.py +0 -0
- {sunholo-0.145.0/src/sunholo/templates/agent → sunholo-0.145.1/src/sunholo/ollama}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/ollama/ollama_images.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/pubsub/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/pubsub/process_pubsub.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/pubsub/pubsub_manager.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/qna/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/qna/parsers.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/qna/retry.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/senses/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/senses/stream_voice.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/streaming/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/streaming/content_buffer.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/streaming/langserve.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/streaming/stream_lookup.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/streaming/streaming.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/summarise/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/summarise/summarise.py +0 -0
- {sunholo-0.145.0/src/sunholo/ollama → sunholo-0.145.1/src/sunholo/templates/agent}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/agent/agent_service.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/agent/app.py +0 -0
- {sunholo-0.145.0/src/sunholo/templates/project → sunholo-0.145.1/src/sunholo/templates/agent}/my_log.py +0 -0
- {sunholo-0.145.0/src/sunholo/lookup → sunholo-0.145.1/src/sunholo/templates/agent/tools}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/agent/tools/your_agent.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/agent/vac_service.py +0 -0
- {sunholo-0.145.0/src/sunholo/langfuse → sunholo-0.145.1/src/sunholo/templates/project}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/project/app.py +0 -0
- {sunholo-0.145.0/src/sunholo/templates/agent → sunholo-0.145.1/src/sunholo/templates/project}/my_log.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/project/vac_service.py +0 -0
- {sunholo-0.145.0/src/sunholo/cli → sunholo-0.145.1/src/sunholo/templates/system_services}/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/system_services/app.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/templates/system_services/my_log.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/terraform/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/terraform/tfvars_editor.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/tools/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/tools/web_browser.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/api_key.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/big_context.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/config.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/config_class.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/config_schema.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/gcp.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/gcp_project.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/mime.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/parsers.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/proto_convert.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/timedelta.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/user_ids.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/utils/version.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/__init__.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/extensions_call.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/extensions_class.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/genai_functions.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/init.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/memory_tools.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/safety.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo/vertex/type_dict_to_json.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo.egg-info/dependency_links.txt +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo.egg-info/entry_points.txt +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/src/sunholo.egg-info/top_level.txt +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/tests/test_async.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/tests/test_async_genai2.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/tests/test_chat_history.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/tests/test_config.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/tests/test_genai2.py +0 -0
- {sunholo-0.145.0 → sunholo-0.145.1}/tests/test_unstructured.py +0 -0
@@ -1,11 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sunholo
|
3
|
-
Version: 0.145.
|
3
|
+
Version: 0.145.1
|
4
4
|
Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
|
5
5
|
Author-email: Holosun ApS <multivac@sunholo.com>
|
6
6
|
License: Apache License, Version 2.0
|
7
7
|
Project-URL: Homepage, https://github.com/sunholo-data/sunholo-py
|
8
|
-
Project-URL: Download, https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz
|
9
8
|
Keywords: llms,devops,google_cloud_platform
|
10
9
|
Classifier: Development Status :: 3 - Alpha
|
11
10
|
Classifier: Intended Audience :: Developers
|
@@ -107,6 +106,7 @@ Requires-Dist: azure-storage-blob; extra == "azure"
|
|
107
106
|
Provides-Extra: cli
|
108
107
|
Requires-Dist: jsonschema>=4.21.1; extra == "cli"
|
109
108
|
Requires-Dist: rich; extra == "cli"
|
109
|
+
Requires-Dist: fastmcp; extra == "cli"
|
110
110
|
Provides-Extra: database
|
111
111
|
Requires-Dist: asyncpg; extra == "database"
|
112
112
|
Requires-Dist: supabase; extra == "database"
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "sunholo"
|
7
|
-
version = "0.145.
|
7
|
+
version = "0.145.1"
|
8
8
|
description = "AI DevOps - a package to help deploy GenAI to the Cloud."
|
9
9
|
readme = "README.md"
|
10
10
|
requires-python = ">=3.11"
|
@@ -33,7 +33,6 @@ dependencies = [
|
|
33
33
|
|
34
34
|
[project.urls]
|
35
35
|
Homepage = "https://github.com/sunholo-data/sunholo-py"
|
36
|
-
Download = "https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz"
|
37
36
|
|
38
37
|
[project.scripts]
|
39
38
|
sunholo = "sunholo.cli.cli:main"
|
@@ -140,7 +139,8 @@ azure = [
|
|
140
139
|
]
|
141
140
|
cli = [
|
142
141
|
"jsonschema>=4.21.1",
|
143
|
-
"rich"
|
142
|
+
"rich",
|
143
|
+
"fastmcp"
|
144
144
|
]
|
145
145
|
database = [
|
146
146
|
"asyncpg",
|
@@ -1,11 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sunholo
|
3
|
-
Version: 0.145.
|
3
|
+
Version: 0.145.1
|
4
4
|
Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
|
5
5
|
Author-email: Holosun ApS <multivac@sunholo.com>
|
6
6
|
License: Apache License, Version 2.0
|
7
7
|
Project-URL: Homepage, https://github.com/sunholo-data/sunholo-py
|
8
|
-
Project-URL: Download, https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.118.0.tar.gz
|
9
8
|
Keywords: llms,devops,google_cloud_platform
|
10
9
|
Classifier: Development Status :: 3 - Alpha
|
11
10
|
Classifier: Intended Audience :: Developers
|
@@ -107,6 +106,7 @@ Requires-Dist: azure-storage-blob; extra == "azure"
|
|
107
106
|
Provides-Extra: cli
|
108
107
|
Requires-Dist: jsonschema>=4.21.1; extra == "cli"
|
109
108
|
Requires-Dist: rich; extra == "cli"
|
109
|
+
Requires-Dist: fastmcp; extra == "cli"
|
110
110
|
Provides-Extra: database
|
111
111
|
Requires-Dist: asyncpg; extra == "database"
|
112
112
|
Requires-Dist: supabase; extra == "database"
|
@@ -195,9 +195,8 @@ src/sunholo/vertex/safety.py
|
|
195
195
|
src/sunholo/vertex/type_dict_to_json.py
|
196
196
|
tests/test_async.py
|
197
197
|
tests/test_async_genai2.py
|
198
|
+
tests/test_async_task_runner.py
|
198
199
|
tests/test_chat_history.py
|
199
200
|
tests/test_config.py
|
200
201
|
tests/test_genai2.py
|
201
|
-
tests/test_unstructured.py
|
202
|
-
tests/test_vac_routes_fastapi.py
|
203
|
-
tests/test_vac_routes_mcp.py
|
202
|
+
tests/test_unstructured.py
|
@@ -0,0 +1,355 @@
|
|
1
|
+
"""
|
2
|
+
Tests for AsyncTaskRunner with default callbacks and enhanced features.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import pytest
|
7
|
+
from unittest.mock import AsyncMock, MagicMock
|
8
|
+
from sunholo.invoke.async_task_runner import AsyncTaskRunner, TaskConfig, CallbackContext
|
9
|
+
from tenacity import stop_after_attempt
|
10
|
+
|
11
|
+
|
12
|
+
# Test fixtures - async functions to use in tests
|
13
|
+
async def simple_task(value: str):
|
14
|
+
"""Simple async task that returns a value."""
|
15
|
+
await asyncio.sleep(0.1)
|
16
|
+
return f"Result: {value}"
|
17
|
+
|
18
|
+
|
19
|
+
async def failing_task(value: str):
|
20
|
+
"""Task that always fails for testing error handling."""
|
21
|
+
await asyncio.sleep(0.1)
|
22
|
+
raise ValueError(f"Intentional error: {value}")
|
23
|
+
|
24
|
+
|
25
|
+
async def slow_task(value: str, duration: float = 2.0):
|
26
|
+
"""Task with configurable duration for timeout testing."""
|
27
|
+
await asyncio.sleep(duration)
|
28
|
+
return f"Slow result: {value}"
|
29
|
+
|
30
|
+
|
31
|
+
@pytest.mark.asyncio
|
32
|
+
async def test_default_callbacks_basic():
|
33
|
+
"""Test that default callbacks populate shared_state correctly."""
|
34
|
+
runner = AsyncTaskRunner(verbose=False) # Quiet for tests
|
35
|
+
|
36
|
+
# Add some tasks
|
37
|
+
runner.add_task(simple_task, "test1")
|
38
|
+
runner.add_task(simple_task, "test2")
|
39
|
+
|
40
|
+
# Run and get results
|
41
|
+
results = await runner.get_aggregated_results()
|
42
|
+
|
43
|
+
# Check that default callbacks populated the state
|
44
|
+
assert 'results' in results
|
45
|
+
assert 'completed' in results
|
46
|
+
assert 'started' in results
|
47
|
+
assert 'errors' in results
|
48
|
+
|
49
|
+
# Check task results
|
50
|
+
assert 'simple_task' in results['results']
|
51
|
+
assert results['results']['simple_task'] == "Result: test2" # Last one wins with same name
|
52
|
+
|
53
|
+
# Check completed list
|
54
|
+
assert len(results['completed']) == 2
|
55
|
+
assert results['completed'].count('simple_task') == 2
|
56
|
+
|
57
|
+
# Check started list
|
58
|
+
assert len(results['started']) == 2
|
59
|
+
|
60
|
+
# No errors expected
|
61
|
+
assert len(results['errors']) == 0
|
62
|
+
|
63
|
+
|
64
|
+
@pytest.mark.asyncio
|
65
|
+
async def test_default_callbacks_with_errors():
|
66
|
+
"""Test that default callbacks handle errors correctly."""
|
67
|
+
runner = AsyncTaskRunner(verbose=False)
|
68
|
+
|
69
|
+
# Add both successful and failing tasks
|
70
|
+
runner.add_task(simple_task, "success")
|
71
|
+
runner.add_task(failing_task, "failure")
|
72
|
+
|
73
|
+
results = await runner.get_aggregated_results()
|
74
|
+
|
75
|
+
# Check successful task
|
76
|
+
assert 'simple_task' in results['results']
|
77
|
+
assert results['results']['simple_task'] == "Result: success"
|
78
|
+
|
79
|
+
# Check error was captured
|
80
|
+
assert 'failing_task' in results['errors']
|
81
|
+
assert "Intentional error: failure" in results['errors']['failing_task']
|
82
|
+
|
83
|
+
# Check completed list (only successful task)
|
84
|
+
assert 'simple_task' in results['completed']
|
85
|
+
|
86
|
+
# Both tasks should have started
|
87
|
+
assert len(results['started']) == 2
|
88
|
+
|
89
|
+
|
90
|
+
@pytest.mark.asyncio
|
91
|
+
async def test_default_callbacks_with_retry():
|
92
|
+
"""Test that default callbacks track retry attempts."""
|
93
|
+
runner = AsyncTaskRunner(
|
94
|
+
verbose=False,
|
95
|
+
retry_enabled=False # Global default
|
96
|
+
)
|
97
|
+
|
98
|
+
# Add task with retry enabled
|
99
|
+
runner.add_task(
|
100
|
+
failing_task,
|
101
|
+
"retry_test",
|
102
|
+
task_config=TaskConfig(
|
103
|
+
retry_enabled=True,
|
104
|
+
retry_kwargs={'stop': stop_after_attempt(3)}
|
105
|
+
)
|
106
|
+
)
|
107
|
+
|
108
|
+
results = await runner.get_aggregated_results()
|
109
|
+
|
110
|
+
# Check that retries were tracked
|
111
|
+
assert 'retries' in results
|
112
|
+
assert len(results['retries']) == 2 # Attempts 2 and 3 (not 1)
|
113
|
+
assert 'failing_task_attempt_2' in results['retries']
|
114
|
+
assert 'failing_task_attempt_3' in results['retries']
|
115
|
+
|
116
|
+
# Task should have error after all retries
|
117
|
+
assert 'failing_task' in results['errors']
|
118
|
+
|
119
|
+
|
120
|
+
@pytest.mark.asyncio
|
121
|
+
async def test_custom_callback_override():
|
122
|
+
"""Test that custom callbacks override defaults correctly."""
|
123
|
+
custom_complete_called = []
|
124
|
+
|
125
|
+
async def custom_on_complete(ctx: CallbackContext):
|
126
|
+
"""Custom completion handler for testing."""
|
127
|
+
custom_complete_called.append(ctx.task_name)
|
128
|
+
# Still populate state like default would
|
129
|
+
ctx.shared_state['results'][ctx.task_name] = f"CUSTOM: {ctx.result}"
|
130
|
+
ctx.shared_state['completed'].append(ctx.task_name)
|
131
|
+
|
132
|
+
runner = AsyncTaskRunner(
|
133
|
+
verbose=False,
|
134
|
+
callbacks={'on_task_complete': custom_on_complete}
|
135
|
+
# Other callbacks remain as defaults
|
136
|
+
)
|
137
|
+
|
138
|
+
runner.add_task(simple_task, "test")
|
139
|
+
results = await runner.get_aggregated_results()
|
140
|
+
|
141
|
+
# Check custom callback was called
|
142
|
+
assert len(custom_complete_called) == 1
|
143
|
+
assert custom_complete_called[0] == 'simple_task'
|
144
|
+
|
145
|
+
# Check custom result format
|
146
|
+
assert results['results']['simple_task'] == "CUSTOM: Result: test"
|
147
|
+
|
148
|
+
# Default callbacks should still work for other events
|
149
|
+
assert 'started' in results
|
150
|
+
assert 'simple_task' in results['started']
|
151
|
+
|
152
|
+
|
153
|
+
@pytest.mark.asyncio
|
154
|
+
async def test_no_default_callbacks():
|
155
|
+
"""Test that disabling default callbacks works."""
|
156
|
+
runner = AsyncTaskRunner(
|
157
|
+
use_default_callbacks=False,
|
158
|
+
verbose=False
|
159
|
+
)
|
160
|
+
|
161
|
+
runner.add_task(simple_task, "test")
|
162
|
+
results = await runner.get_aggregated_results()
|
163
|
+
|
164
|
+
# State should be initialized but empty (no callbacks to populate it)
|
165
|
+
assert results == {
|
166
|
+
'results': {},
|
167
|
+
'errors': {},
|
168
|
+
'completed': [],
|
169
|
+
'started': [],
|
170
|
+
'retries': [],
|
171
|
+
'timed_out': []
|
172
|
+
}
|
173
|
+
|
174
|
+
|
175
|
+
@pytest.mark.asyncio
|
176
|
+
async def test_per_task_timeout_with_defaults():
|
177
|
+
"""Test per-task timeout configuration with default callbacks."""
|
178
|
+
runner = AsyncTaskRunner(
|
179
|
+
timeout=1, # Default 1 second timeout
|
180
|
+
verbose=False
|
181
|
+
)
|
182
|
+
|
183
|
+
# This task should timeout with default
|
184
|
+
runner.add_task(slow_task, "timeout_task", 2.0) # Takes 2 seconds
|
185
|
+
|
186
|
+
# This task should complete with custom timeout
|
187
|
+
runner.add_task(
|
188
|
+
slow_task,
|
189
|
+
"complete_task",
|
190
|
+
0.5, # Takes 0.5 seconds
|
191
|
+
task_config=TaskConfig(timeout=3) # 3 second timeout
|
192
|
+
)
|
193
|
+
|
194
|
+
results = await runner.get_aggregated_results()
|
195
|
+
|
196
|
+
# Check that one timed out
|
197
|
+
assert 'timed_out' in results
|
198
|
+
assert 'slow_task' in results['timed_out']
|
199
|
+
|
200
|
+
# Check that timeout was recorded as error
|
201
|
+
assert 'slow_task' in results['errors']
|
202
|
+
# The default callback stores "Timeout after Xs" or the error might be "Unknown error" if timeout wasn't caught properly
|
203
|
+
error_msg = results['errors'].get('slow_task', '').lower()
|
204
|
+
assert 'timeout' in error_msg or 'unknown' in error_msg
|
205
|
+
|
206
|
+
# The one with extended timeout should complete
|
207
|
+
assert 'slow_task' in results['results']
|
208
|
+
assert results['results']['slow_task'] == "Slow result: complete_task"
|
209
|
+
|
210
|
+
|
211
|
+
@pytest.mark.asyncio
|
212
|
+
async def test_shared_state_persistence():
|
213
|
+
"""Test that shared_state is accessible and modifiable across callbacks."""
|
214
|
+
shared_state = {
|
215
|
+
'custom_counter': 0,
|
216
|
+
'task_order': []
|
217
|
+
}
|
218
|
+
|
219
|
+
async def counting_callback(ctx: CallbackContext):
|
220
|
+
"""Callback that increments a counter."""
|
221
|
+
ctx.shared_state['custom_counter'] += 1
|
222
|
+
ctx.shared_state['task_order'].append(ctx.task_name)
|
223
|
+
# Also do the default behavior
|
224
|
+
ctx.shared_state.setdefault('results', {})[ctx.task_name] = ctx.result
|
225
|
+
|
226
|
+
runner = AsyncTaskRunner(
|
227
|
+
shared_state=shared_state,
|
228
|
+
callbacks={'on_task_complete': counting_callback},
|
229
|
+
verbose=False
|
230
|
+
)
|
231
|
+
|
232
|
+
runner.add_task(simple_task, "first")
|
233
|
+
runner.add_task(simple_task, "second")
|
234
|
+
runner.add_task(simple_task, "third")
|
235
|
+
|
236
|
+
results = await runner.get_aggregated_results()
|
237
|
+
|
238
|
+
# Check custom state was maintained
|
239
|
+
assert results['custom_counter'] == 3
|
240
|
+
assert len(results['task_order']) == 3
|
241
|
+
assert all(name == 'simple_task' for name in results['task_order'])
|
242
|
+
|
243
|
+
# Default keys should also be present
|
244
|
+
assert 'results' in results
|
245
|
+
assert 'errors' in results
|
246
|
+
assert 'completed' in results
|
247
|
+
|
248
|
+
|
249
|
+
@pytest.mark.asyncio
|
250
|
+
async def test_verbose_mode():
|
251
|
+
"""Test that verbose mode affects shared_state population but not its behavior."""
|
252
|
+
# Test verbose=True (default)
|
253
|
+
runner_verbose = AsyncTaskRunner(verbose=True)
|
254
|
+
runner_verbose.add_task(simple_task, "verbose_test")
|
255
|
+
results_verbose = await runner_verbose.get_aggregated_results()
|
256
|
+
|
257
|
+
# Should have results regardless of verbose mode
|
258
|
+
assert 'simple_task' in results_verbose['results']
|
259
|
+
assert 'simple_task' in results_verbose['completed']
|
260
|
+
|
261
|
+
# Test verbose=False - should still work but quietly
|
262
|
+
runner_quiet = AsyncTaskRunner(verbose=False)
|
263
|
+
runner_quiet.add_task(simple_task, "quiet_test")
|
264
|
+
results_quiet = await runner_quiet.get_aggregated_results()
|
265
|
+
|
266
|
+
# Should have same results structure
|
267
|
+
assert 'simple_task' in results_quiet['results']
|
268
|
+
assert 'simple_task' in results_quiet['completed']
|
269
|
+
|
270
|
+
# Both should produce same result structure
|
271
|
+
assert set(results_verbose.keys()) == set(results_quiet.keys())
|
272
|
+
|
273
|
+
|
274
|
+
@pytest.mark.asyncio
|
275
|
+
async def test_multiple_tasks_same_name():
|
276
|
+
"""Test behavior when multiple tasks have the same function name."""
|
277
|
+
runner = AsyncTaskRunner(verbose=False)
|
278
|
+
|
279
|
+
# Add multiple tasks with same function
|
280
|
+
runner.add_task(simple_task, "first")
|
281
|
+
runner.add_task(simple_task, "second")
|
282
|
+
runner.add_task(simple_task, "third")
|
283
|
+
|
284
|
+
results = await runner.get_aggregated_results()
|
285
|
+
|
286
|
+
# Results dict should have the last result (overwrites)
|
287
|
+
assert results['results']['simple_task'] == "Result: third"
|
288
|
+
|
289
|
+
# Completed list should have all three
|
290
|
+
assert results['completed'].count('simple_task') == 3
|
291
|
+
|
292
|
+
# Started list should have all three
|
293
|
+
assert results['started'].count('simple_task') == 3
|
294
|
+
|
295
|
+
|
296
|
+
@pytest.mark.asyncio
|
297
|
+
async def test_empty_runner():
|
298
|
+
"""Test that runner works with no tasks."""
|
299
|
+
runner = AsyncTaskRunner(verbose=False)
|
300
|
+
|
301
|
+
# Run with no tasks
|
302
|
+
results = await runner.get_aggregated_results()
|
303
|
+
|
304
|
+
# Should return empty but initialized state
|
305
|
+
assert results == {
|
306
|
+
'results': {},
|
307
|
+
'errors': {},
|
308
|
+
'completed': [],
|
309
|
+
'started': [],
|
310
|
+
'retries': [],
|
311
|
+
'timed_out': []
|
312
|
+
}
|
313
|
+
|
314
|
+
|
315
|
+
@pytest.mark.asyncio
|
316
|
+
async def test_task_config_none_values():
|
317
|
+
"""Test that TaskConfig with None values falls back to global settings."""
|
318
|
+
runner = AsyncTaskRunner(
|
319
|
+
timeout=5,
|
320
|
+
retry_enabled=True,
|
321
|
+
verbose=False
|
322
|
+
)
|
323
|
+
|
324
|
+
# Add task with partial config (None values should use globals)
|
325
|
+
runner.add_task(
|
326
|
+
simple_task,
|
327
|
+
"test",
|
328
|
+
task_config=TaskConfig(
|
329
|
+
timeout=None, # Should use global (5)
|
330
|
+
retry_enabled=None, # Should use global (True)
|
331
|
+
metadata={'custom': 'data'}
|
332
|
+
)
|
333
|
+
)
|
334
|
+
|
335
|
+
results = await runner.get_aggregated_results()
|
336
|
+
|
337
|
+
# Task should complete successfully
|
338
|
+
assert 'simple_task' in results['results']
|
339
|
+
assert results['results']['simple_task'] == "Result: test"
|
340
|
+
|
341
|
+
|
342
|
+
if __name__ == "__main__":
|
343
|
+
# Run tests with asyncio
|
344
|
+
asyncio.run(test_default_callbacks_basic())
|
345
|
+
asyncio.run(test_default_callbacks_with_errors())
|
346
|
+
asyncio.run(test_default_callbacks_with_retry())
|
347
|
+
asyncio.run(test_custom_callback_override())
|
348
|
+
asyncio.run(test_no_default_callbacks())
|
349
|
+
asyncio.run(test_per_task_timeout_with_defaults())
|
350
|
+
asyncio.run(test_shared_state_persistence())
|
351
|
+
asyncio.run(test_verbose_mode())
|
352
|
+
asyncio.run(test_multiple_tasks_same_name())
|
353
|
+
asyncio.run(test_empty_runner())
|
354
|
+
asyncio.run(test_task_config_none_values())
|
355
|
+
print("All tests passed!")
|
@@ -1,226 +0,0 @@
|
|
1
|
-
# Copyright [2024] [Holosun ApS]
|
2
|
-
#
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
-
# you may not use this file except in compliance with the License.
|
5
|
-
# You may obtain a copy of the License at
|
6
|
-
#
|
7
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
-
#
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
-
# See the License for the specific language governing permissions and
|
13
|
-
# limitations under the License.
|
14
|
-
|
15
|
-
import pytest
|
16
|
-
import json
|
17
|
-
import asyncio
|
18
|
-
from unittest.mock import patch, MagicMock, AsyncMock
|
19
|
-
|
20
|
-
# Skip entire test file if FastAPI is not available
|
21
|
-
pytest.importorskip("fastapi")
|
22
|
-
pytest.importorskip("httpx")
|
23
|
-
|
24
|
-
from fastapi import FastAPI
|
25
|
-
from fastapi.testclient import TestClient
|
26
|
-
from httpx import AsyncClient, ASGITransport
|
27
|
-
|
28
|
-
from src.sunholo.agents.fastapi.vac_routes import VACRoutesFastAPI, VACRequest
|
29
|
-
from tests.fixtures.mock_interpreters import (
|
30
|
-
mock_async_stream_interpreter,
|
31
|
-
mock_sync_stream_interpreter,
|
32
|
-
mock_async_vac_interpreter,
|
33
|
-
mock_sync_vac_interpreter,
|
34
|
-
mock_async_stream_with_heartbeat,
|
35
|
-
mock_async_stream_with_error,
|
36
|
-
mock_sync_stream_with_timeout
|
37
|
-
)
|
38
|
-
|
39
|
-
|
40
|
-
@pytest.fixture
|
41
|
-
def fastapi_app_async():
|
42
|
-
"""Create a FastAPI app with async interpreters."""
|
43
|
-
app = FastAPI()
|
44
|
-
vac_routes = VACRoutesFastAPI(
|
45
|
-
app,
|
46
|
-
stream_interpreter=mock_async_stream_interpreter,
|
47
|
-
vac_interpreter=mock_async_vac_interpreter,
|
48
|
-
enable_mcp_server=False,
|
49
|
-
add_langfuse_eval=False
|
50
|
-
)
|
51
|
-
return app
|
52
|
-
|
53
|
-
|
54
|
-
@pytest.fixture
|
55
|
-
def fastapi_app_sync():
|
56
|
-
"""Create a FastAPI app with sync interpreters."""
|
57
|
-
app = FastAPI()
|
58
|
-
vac_routes = VACRoutesFastAPI(
|
59
|
-
app,
|
60
|
-
stream_interpreter=mock_sync_stream_interpreter,
|
61
|
-
vac_interpreter=mock_sync_vac_interpreter,
|
62
|
-
enable_mcp_server=False,
|
63
|
-
add_langfuse_eval=False
|
64
|
-
)
|
65
|
-
return app
|
66
|
-
|
67
|
-
|
68
|
-
@pytest.fixture
|
69
|
-
def test_client_async(fastapi_app_async):
|
70
|
-
"""Create test client for async app."""
|
71
|
-
return TestClient(fastapi_app_async)
|
72
|
-
|
73
|
-
|
74
|
-
@pytest.fixture
|
75
|
-
def test_client_sync(fastapi_app_sync):
|
76
|
-
"""Create test client for sync app."""
|
77
|
-
return TestClient(fastapi_app_sync)
|
78
|
-
|
79
|
-
|
80
|
-
class TestVACRoutesFastAPI:
|
81
|
-
"""Test suite for VACRoutesFastAPI."""
|
82
|
-
|
83
|
-
def test_health_endpoint(self, test_client_async):
|
84
|
-
"""Test health check endpoint."""
|
85
|
-
response = test_client_async.get("/health")
|
86
|
-
assert response.status_code == 200
|
87
|
-
assert response.json() == {"status": "healthy"}
|
88
|
-
|
89
|
-
def test_home_endpoint(self, test_client_async):
|
90
|
-
"""Test home endpoint."""
|
91
|
-
response = test_client_async.get("/")
|
92
|
-
assert response.status_code == 200
|
93
|
-
assert response.json() == "OK"
|
94
|
-
|
95
|
-
@pytest.mark.asyncio
|
96
|
-
@patch('src.sunholo.agents.fastapi.vac_routes.ConfigManager')
|
97
|
-
@patch('src.sunholo.agents.fastapi.vac_routes.extract_chat_history_async_cached')
|
98
|
-
@patch('src.sunholo.agents.fastapi.vac_routes.archive_qa')
|
99
|
-
async def test_async_streaming_plain(self, mock_archive, mock_extract, mock_config, test_client_async):
|
100
|
-
"""Test async streaming with plain text response."""
|
101
|
-
# Setup mocks
|
102
|
-
mock_config.return_value = MagicMock()
|
103
|
-
mock_extract.return_value = []
|
104
|
-
mock_archive.return_value = AsyncMock()
|
105
|
-
|
106
|
-
# Make request
|
107
|
-
request_data = {
|
108
|
-
"user_input": "Test question",
|
109
|
-
"chat_history": [],
|
110
|
-
"stream_wait_time": 1,
|
111
|
-
"stream_timeout": 10
|
112
|
-
}
|
113
|
-
|
114
|
-
response = test_client_async.post(
|
115
|
-
"/vac/streaming/test_vac",
|
116
|
-
json=request_data
|
117
|
-
)
|
118
|
-
|
119
|
-
assert response.status_code == 200
|
120
|
-
assert response.headers["content-type"] == "text/plain; charset=utf-8"
|
121
|
-
|
122
|
-
# Check streaming content
|
123
|
-
content = response.text
|
124
|
-
assert "Hello from async interpreter!" in content
|
125
|
-
assert "Test question" in content
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
def test_openai_health(self, test_client_async):
|
132
|
-
"""Test OpenAI health endpoint."""
|
133
|
-
response = test_client_async.get("/openai/health")
|
134
|
-
assert response.status_code == 200
|
135
|
-
assert response.json() == {"message": "Success"}
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
def test_vac_request_model(self):
|
140
|
-
"""Test VACRequest model validation."""
|
141
|
-
# Valid request
|
142
|
-
request = VACRequest(
|
143
|
-
user_input="Test",
|
144
|
-
chat_history=[],
|
145
|
-
stream_wait_time=5,
|
146
|
-
stream_timeout=30
|
147
|
-
)
|
148
|
-
assert request.user_input == "Test"
|
149
|
-
assert request.stream_wait_time == 5
|
150
|
-
|
151
|
-
# Default values
|
152
|
-
request = VACRequest(user_input="Test")
|
153
|
-
assert request.chat_history is None
|
154
|
-
assert request.stream_wait_time == 7
|
155
|
-
assert request.stream_timeout == 120
|
156
|
-
|
157
|
-
@pytest.mark.asyncio
|
158
|
-
async def test_streaming_with_heartbeat(self):
|
159
|
-
"""Test streaming with heartbeat tokens."""
|
160
|
-
app = FastAPI()
|
161
|
-
vac_routes = VACRoutesFastAPI(
|
162
|
-
app,
|
163
|
-
stream_interpreter=mock_async_stream_with_heartbeat,
|
164
|
-
enable_mcp_server=False,
|
165
|
-
add_langfuse_eval=False
|
166
|
-
)
|
167
|
-
|
168
|
-
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
169
|
-
request_data = {
|
170
|
-
"user_input": "Test heartbeat",
|
171
|
-
"chat_history": [],
|
172
|
-
"stream_wait_time": 1,
|
173
|
-
"stream_timeout": 10
|
174
|
-
}
|
175
|
-
|
176
|
-
with patch('src.sunholo.agents.fastapi.vac_routes.ConfigManager'):
|
177
|
-
with patch('src.sunholo.agents.fastapi.vac_routes.extract_chat_history_async_cached', return_value=[]):
|
178
|
-
with patch('src.sunholo.agents.fastapi.vac_routes.archive_qa', new_callable=AsyncMock):
|
179
|
-
response = await client.post(
|
180
|
-
"/vac/streaming/test_vac",
|
181
|
-
json=request_data
|
182
|
-
)
|
183
|
-
|
184
|
-
assert response.status_code == 200
|
185
|
-
# Heartbeat tokens should be processed
|
186
|
-
content = response.text
|
187
|
-
assert "Starting response..." in content
|
188
|
-
|
189
|
-
|
190
|
-
def test_interpreter_detection(self):
|
191
|
-
"""Test that sync/async interpreter detection works correctly."""
|
192
|
-
app = FastAPI()
|
193
|
-
|
194
|
-
# Test async detection
|
195
|
-
vac_routes_async = VACRoutesFastAPI(
|
196
|
-
app,
|
197
|
-
stream_interpreter=mock_async_stream_interpreter,
|
198
|
-
vac_interpreter=mock_async_vac_interpreter
|
199
|
-
)
|
200
|
-
assert vac_routes_async.stream_is_async is True
|
201
|
-
assert vac_routes_async.vac_is_async is True
|
202
|
-
|
203
|
-
# Test sync detection
|
204
|
-
vac_routes_sync = VACRoutesFastAPI(
|
205
|
-
app,
|
206
|
-
stream_interpreter=mock_sync_stream_interpreter,
|
207
|
-
vac_interpreter=mock_sync_vac_interpreter
|
208
|
-
)
|
209
|
-
assert vac_routes_sync.stream_is_async is False
|
210
|
-
assert vac_routes_sync.vac_is_async is False
|
211
|
-
|
212
|
-
def test_mcp_server_disabled(self, test_client_async):
|
213
|
-
"""Test that MCP server endpoints return 404 when disabled."""
|
214
|
-
response = test_client_async.post("/mcp")
|
215
|
-
assert response.status_code == 404
|
216
|
-
|
217
|
-
response = test_client_async.get("/mcp")
|
218
|
-
assert response.status_code == 404
|
219
|
-
|
220
|
-
def test_a2a_agent_disabled(self, test_client_async):
|
221
|
-
"""Test that A2A agent endpoints return 404 when disabled."""
|
222
|
-
response = test_client_async.get("/.well-known/agent.json")
|
223
|
-
assert response.status_code == 404
|
224
|
-
|
225
|
-
response = test_client_async.post("/a2a/tasks/send")
|
226
|
-
assert response.status_code == 404
|