iatoolkit 2.59.0__tar.gz → 2.60.0__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.
- {iatoolkit-2.59.0/src/iatoolkit.egg-info → iatoolkit-2.60.0}/PKG-INFO +1 -1
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/__init__.py +1 -1
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_tools_pack.yaml +87 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/core.py +4 -0
- iatoolkit-2.60.0/src/iatoolkit/infra/connectors/google_drive_connector.py +308 -0
- iatoolkit-2.60.0/src/iatoolkit/repositories/memory_question_repo.py +121 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/models.py +62 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/context_builder_service.py +3 -1
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/knowledge_base_service.py +158 -0
- iatoolkit-2.60.0/src/iatoolkit/services/memory_question_service.py +449 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/tool_service.py +40 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0/src/iatoolkit.egg-info}/PKG-INFO +1 -1
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit.egg-info/SOURCES.txt +2 -0
- iatoolkit-2.59.0/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -118
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/LICENSE +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/pyproject.toml +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/readme.md +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/setup.cfg +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/base_company.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/cli_commands.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/bridge_validation.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/company_paths.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/exceptions.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/asset_storage.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/database_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/memory_compilation_trigger.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/secret_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/signup_policy_resolver.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/interfaces/web_search_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/model_registry.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/routes.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/secret_resolver.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/session_manager.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/text_splitter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/url_safety.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/common/util.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/company_registry.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/llm_capabilities.yaml +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/channel_business_identity.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/channel_conversation_continuity.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/channel_response_style.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/chat_state_rules.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/chat_user_profile.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/core_identity.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/email_output.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/file_download_output.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/format_styles.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/html_structures.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/links_documents.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/memory_usage.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/multimodal_basics.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/output_basics.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/presentation_formatting.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/query_main.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_aggregation_scope.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_casting.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_core.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_jsonb.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_mysql_casting.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_mysql_json.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_redshift_basics.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_redshift_types.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/sql_rules.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts/tool_html_passthrough.prompt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/config/system_prompts_pack.yaml +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/brevo_mail_app.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/call_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/connectors/local_file_connector.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/google_auth_client.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/inference_embeddings_client.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/inference_handler.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/jina_embeddings_client.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_gateway_resolver.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/anthropic_adapter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/deepseek_adapter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/gemini_adapter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/openai_adapter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/openai_compatible_chat_adapter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_providers/openrouter_adapter.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/llm_response.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/locales/en.yaml +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/locales/es.yaml +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/api_key_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/bridge_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/database_manager.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/document_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/env_secret_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/filesystem_asset_repository.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/knowledge_wiki_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/mcp_token_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/memory_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/prompt_resource_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/sql_dataset_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/sql_source_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/runtime_logging.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/api_key_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/attachment_policy_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/auth_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/branding_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/company_context_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/configuration_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/embedding_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/excel_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/file_processor_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/history_manager_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/http_tool_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/i18n_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/inference_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/jwt_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/language_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/license_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/llm_client_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/mail_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/markdown_wiki_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/mcp_token_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/memory_compiler_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/memory_lookup_policy_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/memory_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/memory_wiki_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/noop_memory_compilation_trigger.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/operator_identity_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/contracts.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/image_normalizer.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/parsing_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/pdf_ocr_detection.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/provider_factory.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/provider_resolver.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/providers/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/providers/basic_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/providers/docling_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/parsers/validator.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/pdf_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/profile_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/prompt_resource_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/prompt_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/query_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/signup_policy_resolver.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/sql_dataset_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/sql_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/sql_source_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/storage_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/structured_output_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/system_prompt_catalog.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/system_tools.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/telemetry_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/tenant_wiki_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/tool_output_contract.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/user_session_context_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/visual_kb_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/visual_tool_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/warmup_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/web_search/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/web_search/provider_factory.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/web_search/providers/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/web_search/providers/brave_provider.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/services/web_search_service.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/images/favicon.ico +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/images/favicon.png +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/images/iatoolkit_core.png +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/images/iatoolkit_logo.png +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_feedback_button.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_header_navigation.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_help_content.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_history_button.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_logout_button.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_main.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_memory_button.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_model_selector.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_onboarding_button.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_prompt_manager.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/js/chat_reload_button.js +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/account.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/chat_public.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/documents.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/_chat_header.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/_company_header.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/_login_widget.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/account.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/base.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/change_password.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/chat.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/chat_memory_modal.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/chat_modals.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/error.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/forgot_password.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/home_hosted_default.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/login_global.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/memory/wiki_schema.md +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/pdf/base.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/pdf/letter.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/pdf/report.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/pdf/simple.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/templates/signup.html +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/__init__.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/account_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/api_key_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/base_login_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/categories_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/change_password_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/chat_context_preview_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/chat_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/configuration_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/connectors_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/embedding_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/help_content_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/history_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/home_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/init_context_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/llmquery_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/load_document_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/login_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/logout_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/memory_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/profile_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/prompt_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/prompt_context_preview_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/prompt_resource_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/rag_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/root_redirect_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/signup_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/static_page_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/tool_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/users_api_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit/views/verify_user_view.py +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit.egg-info/requires.txt +0 -0
- {iatoolkit-2.59.0 → iatoolkit-2.60.0}/src/iatoolkit.egg-info/top_level.txt +0 -0
|
@@ -282,6 +282,93 @@ tools:
|
|
|
282
282
|
description: "Opcional en la práctica. Usa null cuando no necesites filtrar por metadata. No inventes filtros ni envíes [] por defecto. Ejemplo: [{'key':'doc.type','value':'invoice'},{'key':'chunk.source_type','value':'table'}]."
|
|
283
283
|
required:
|
|
284
284
|
- query
|
|
285
|
+
- function_name: iat_document_list
|
|
286
|
+
description: Lista los documentos de una colección de la empresa con su ficha (título, resumen, tipo, vigencia), del más reciente al más antiguo. Úsala para preguntas de catálogo ("¿qué contratos tenemos?", "¿cuál es la lista de precios vigente?", "¿qué hay en la carpeta Comercial?"), no para buscar un dato dentro de los documentos (para eso, `iat_document_search`). Los documentos reemplazados u obsoletos no aparecen salvo que lo pidas.
|
|
287
|
+
routing:
|
|
288
|
+
force_include_capability: has_kb_collections
|
|
289
|
+
routing_profile:
|
|
290
|
+
tags:
|
|
291
|
+
- document
|
|
292
|
+
- catalog
|
|
293
|
+
- list
|
|
294
|
+
- knowledge
|
|
295
|
+
intents:
|
|
296
|
+
- List the documents of a company collection with their cards.
|
|
297
|
+
- Find which documents exist of a given type, or which one is valid today.
|
|
298
|
+
examples:
|
|
299
|
+
- iat_document_list(collection="comercial", document_type="contrato")
|
|
300
|
+
- iat_document_list(collection="legal", valid_on="2026-10-14")
|
|
301
|
+
cost:
|
|
302
|
+
penalty: 0.05
|
|
303
|
+
latency_class: low
|
|
304
|
+
expensive: false
|
|
305
|
+
parameters:
|
|
306
|
+
type: object
|
|
307
|
+
properties:
|
|
308
|
+
collection:
|
|
309
|
+
anyOf:
|
|
310
|
+
- type: string
|
|
311
|
+
- type: "null"
|
|
312
|
+
description: Colección a listar (su nombre exacto). null lista todas.
|
|
313
|
+
document_type:
|
|
314
|
+
anyOf:
|
|
315
|
+
- type: string
|
|
316
|
+
- type: "null"
|
|
317
|
+
description: Opcional. Tipo de documento según su ficha (ej. contrato, política, lista de precios, procedimiento).
|
|
318
|
+
text:
|
|
319
|
+
anyOf:
|
|
320
|
+
- type: string
|
|
321
|
+
- type: "null"
|
|
322
|
+
description: Opcional. Texto a buscar en el nombre, título, resumen o palabras clave de la ficha.
|
|
323
|
+
valid_on:
|
|
324
|
+
anyOf:
|
|
325
|
+
- type: string
|
|
326
|
+
- type: "null"
|
|
327
|
+
description: Opcional. Fecha YYYY-MM-DD; deja los documentos vigentes ese día o sin vigencia indicada.
|
|
328
|
+
include_inactive:
|
|
329
|
+
type: boolean
|
|
330
|
+
description: Incluir documentos reemplazados u obsoletos. Por defecto false.
|
|
331
|
+
limit:
|
|
332
|
+
type: integer
|
|
333
|
+
description: Cuántos documentos devolver (máximo 100). Por defecto 30.
|
|
334
|
+
offset:
|
|
335
|
+
type: integer
|
|
336
|
+
description: Desde qué posición seguir (usa next_offset de la respuesta anterior).
|
|
337
|
+
required:
|
|
338
|
+
- collection
|
|
339
|
+
- function_name: iat_document_read
|
|
340
|
+
description: Lee el texto de un documento de la empresa, completo o por partes, con su ficha. Úsala cuando la respuesta necesite el documento entero (resumirlo, compararlo, revisar todas sus cláusulas) y no solo los fragmentos que trae `iat_document_search`. El document_id viene de `iat_document_search` o `iat_document_list`. Si la respuesta trae has_more=true, sigue con offset=next_offset.
|
|
341
|
+
routing:
|
|
342
|
+
force_include_capability: has_kb_collections
|
|
343
|
+
routing_profile:
|
|
344
|
+
tags:
|
|
345
|
+
- document
|
|
346
|
+
- read
|
|
347
|
+
- knowledge
|
|
348
|
+
intents:
|
|
349
|
+
- Read a whole company document, or a part of it, by id.
|
|
350
|
+
- Summarize or compare full documents.
|
|
351
|
+
examples:
|
|
352
|
+
- iat_document_read(document_id=42)
|
|
353
|
+
- iat_document_read(document_id=42, offset=20000)
|
|
354
|
+
cost:
|
|
355
|
+
penalty: 0.1
|
|
356
|
+
latency_class: medium
|
|
357
|
+
expensive: false
|
|
358
|
+
parameters:
|
|
359
|
+
type: object
|
|
360
|
+
properties:
|
|
361
|
+
document_id:
|
|
362
|
+
type: integer
|
|
363
|
+
description: Identificador del documento, devuelto por `iat_document_search` o `iat_document_list`.
|
|
364
|
+
offset:
|
|
365
|
+
type: integer
|
|
366
|
+
description: Desde qué carácter leer. Por defecto 0.
|
|
367
|
+
limit:
|
|
368
|
+
type: integer
|
|
369
|
+
description: Cuántos caracteres leer (máximo 60000). Por defecto 20000.
|
|
370
|
+
required:
|
|
371
|
+
- document_id
|
|
285
372
|
- function_name: iat_wiki_search
|
|
286
373
|
description: Busca páginas publicadas dentro de las wikis de conocimiento compartidas por la empresa. Úsala como acelerador cuando necesites encontrar rápidamente una página relevante, pero no como punto de entrada por defecto. Si no conoces un `path` válido, primero llama `iat_wiki_get_page` con `path="/"` para leer el índice raíz de la wiki y seguir sus links exactos. `wiki_key` es el identificador estable de una wiki publicada, por ejemplo `sales`, `ops` o `hr`; úsalo cuando ya sepas en qué wiki buscar y omítelo cuando quieras buscar en todas las wikis publicadas disponibles en esta conexión MCP. Esta tool devuelve candidatos; luego usa `iat_wiki_get_page` para leer la página completa antes de responder.
|
|
287
374
|
routing:
|
|
@@ -392,6 +392,7 @@ class IAToolkit:
|
|
|
392
392
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
393
393
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
394
394
|
from iatoolkit.repositories.memory_repo import MemoryRepo
|
|
395
|
+
from iatoolkit.repositories.memory_question_repo import MemoryQuestionRepo
|
|
395
396
|
from iatoolkit.repositories.mcp_token_repo import McpTokenRepo
|
|
396
397
|
from iatoolkit.repositories.sql_dataset_repo import SqlDatasetRepo
|
|
397
398
|
from iatoolkit.repositories.sql_source_repo import SqlSourceRepo
|
|
@@ -407,6 +408,7 @@ class IAToolkit:
|
|
|
407
408
|
binder.bind(ProfileRepo, to=ProfileRepo)
|
|
408
409
|
binder.bind(LLMQueryRepo, to=LLMQueryRepo)
|
|
409
410
|
binder.bind(MemoryRepo, to=MemoryRepo)
|
|
411
|
+
binder.bind(MemoryQuestionRepo, to=MemoryQuestionRepo)
|
|
410
412
|
binder.bind(McpTokenRepo, to=McpTokenRepo)
|
|
411
413
|
binder.bind(SqlDatasetRepo, to=SqlDatasetRepo)
|
|
412
414
|
binder.bind(SqlSourceRepo, to=SqlSourceRepo)
|
|
@@ -436,6 +438,7 @@ class IAToolkit:
|
|
|
436
438
|
from iatoolkit.services.pdf_service import PdfService
|
|
437
439
|
from iatoolkit.services.mail_service import MailService
|
|
438
440
|
from iatoolkit.services.memory_compiler_service import MemoryCompilerService
|
|
441
|
+
from iatoolkit.services.memory_question_service import MemoryQuestionService
|
|
439
442
|
from iatoolkit.services.memory_service import MemoryService
|
|
440
443
|
from iatoolkit.services.mcp_token_service import McpTokenService
|
|
441
444
|
from iatoolkit.services.memory_wiki_service import MemoryWikiService
|
|
@@ -481,6 +484,7 @@ class IAToolkit:
|
|
|
481
484
|
binder.bind(MailService, to=MailService)
|
|
482
485
|
binder.bind(MemoryWikiService, to=MemoryWikiService)
|
|
483
486
|
binder.bind(MemoryCompilerService, to=MemoryCompilerService)
|
|
487
|
+
binder.bind(MemoryQuestionService, to=MemoryQuestionService)
|
|
484
488
|
binder.bind(MemoryService, to=MemoryService)
|
|
485
489
|
binder.bind(McpTokenService, to=McpTokenService)
|
|
486
490
|
binder.bind(ProfileService, to=ProfileService)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Iterator, List
|
|
11
|
+
|
|
12
|
+
from google.oauth2.service_account import Credentials
|
|
13
|
+
from googleapiclient.discovery import build
|
|
14
|
+
from googleapiclient.errors import HttpError
|
|
15
|
+
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
|
|
16
|
+
|
|
17
|
+
from iatoolkit.infra.connectors.file_connector import FileConnector
|
|
18
|
+
|
|
19
|
+
SCOPE_FULL = "https://www.googleapis.com/auth/drive"
|
|
20
|
+
SCOPE_READ_ONLY = "https://www.googleapis.com/auth/drive.readonly"
|
|
21
|
+
|
|
22
|
+
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
|
|
23
|
+
SHORTCUT_MIME_TYPE = "application/vnd.google-apps.shortcut"
|
|
24
|
+
GOOGLE_APPS_PREFIX = "application/vnd.google-apps."
|
|
25
|
+
|
|
26
|
+
# Google's own formats have no bytes to download: they are exported. Each one
|
|
27
|
+
# goes to a format the document parsers already read.
|
|
28
|
+
EXPORT_FORMATS = {
|
|
29
|
+
"application/vnd.google-apps.document": (
|
|
30
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx"),
|
|
31
|
+
"application/vnd.google-apps.spreadsheet": (
|
|
32
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"),
|
|
33
|
+
"application/vnd.google-apps.presentation": ("application/pdf", ".pdf"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
FILE_FIELDS = "id, name, mimeType, parents, md5Checksum, modifiedTime, size, trashed, headRevisionId"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class DriveFile:
|
|
41
|
+
"""A file as Drive describes it, with its path below the folder it was found from."""
|
|
42
|
+
id: str
|
|
43
|
+
name: str
|
|
44
|
+
mime_type: str
|
|
45
|
+
path: str = ""
|
|
46
|
+
parents: tuple[str, ...] = ()
|
|
47
|
+
md5_checksum: str | None = None
|
|
48
|
+
modified_time: str | None = None
|
|
49
|
+
size: int | None = None
|
|
50
|
+
trashed: bool = False
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_folder(self) -> bool:
|
|
54
|
+
return self.mime_type == FOLDER_MIME_TYPE
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_shortcut(self) -> bool:
|
|
58
|
+
return self.mime_type == SHORTCUT_MIME_TYPE
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_google_native(self) -> bool:
|
|
62
|
+
return self.mime_type.startswith(GOOGLE_APPS_PREFIX)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def exportable(self) -> bool:
|
|
66
|
+
return self.mime_type in EXPORT_FORMATS
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def fingerprint(self) -> str:
|
|
70
|
+
"""What tells one version of the file from the next: the content hash for
|
|
71
|
+
uploaded files, the modification time for Google's own formats (they have
|
|
72
|
+
no md5)."""
|
|
73
|
+
return str(self.md5_checksum or self.modified_time or "")
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_api(cls, payload: dict, *, path: str = "") -> "DriveFile":
|
|
77
|
+
size = payload.get("size")
|
|
78
|
+
return cls(
|
|
79
|
+
id=str(payload.get("id") or ""),
|
|
80
|
+
name=str(payload.get("name") or ""),
|
|
81
|
+
mime_type=str(payload.get("mimeType") or ""),
|
|
82
|
+
path=path,
|
|
83
|
+
parents=tuple(payload.get("parents") or ()),
|
|
84
|
+
md5_checksum=payload.get("md5Checksum"),
|
|
85
|
+
modified_time=payload.get("modifiedTime"),
|
|
86
|
+
size=int(size) if str(size or "").isdigit() else None,
|
|
87
|
+
trashed=bool(payload.get("trashed")),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class DriveChange:
|
|
93
|
+
file_id: str
|
|
94
|
+
removed: bool
|
|
95
|
+
file: DriveFile | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class DriveChangesPage:
|
|
100
|
+
changes: list[DriveChange] = field(default_factory=list)
|
|
101
|
+
next_page_token: str | None = None
|
|
102
|
+
new_start_page_token: str | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class GoogleDriveConnector(FileConnector):
|
|
106
|
+
PAGE_SIZE = 1000
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
folder_id: str,
|
|
111
|
+
service_account_path: str = "service_account.json",
|
|
112
|
+
service_account_info: dict | None = None,
|
|
113
|
+
read_only: bool = False,
|
|
114
|
+
):
|
|
115
|
+
"""
|
|
116
|
+
Google Drive through a service account.
|
|
117
|
+
|
|
118
|
+
:param folder_id: the folder `list_files` lists and `upload_file` writes to.
|
|
119
|
+
:param read_only: ask only for `drive.readonly` (whoever only reads, such
|
|
120
|
+
as Memoria's sync, should); the default keeps the full scope that
|
|
121
|
+
uploads and deletes need.
|
|
122
|
+
"""
|
|
123
|
+
self.folder_id = folder_id
|
|
124
|
+
self.service_account_path = service_account_path
|
|
125
|
+
self.service_account_info = service_account_info
|
|
126
|
+
self.scopes = [SCOPE_READ_ONLY if read_only else SCOPE_FULL]
|
|
127
|
+
self.drive_service = self._authenticate()
|
|
128
|
+
|
|
129
|
+
def _authenticate(self):
|
|
130
|
+
if self.service_account_info:
|
|
131
|
+
credentials = Credentials.from_service_account_info(self.service_account_info, scopes=self.scopes)
|
|
132
|
+
else:
|
|
133
|
+
credentials = Credentials.from_service_account_file(self.service_account_path, scopes=self.scopes)
|
|
134
|
+
return build('drive', 'v3', credentials=credentials, cache_discovery=False)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def service_account_email(self) -> str | None:
|
|
138
|
+
info = self.service_account_info or {}
|
|
139
|
+
return str(info.get("client_email") or "").strip() or None
|
|
140
|
+
|
|
141
|
+
# --- FileConnector ------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def list_files(self, prefix: str | None = None) -> List[dict]:
|
|
144
|
+
"""
|
|
145
|
+
Every direct child of the configured folder, as dicts with 'path' (the
|
|
146
|
+
file id), 'name' and 'metadata'. Follows every page of results: a folder
|
|
147
|
+
with more than one page used to be cut silently at the first.
|
|
148
|
+
"""
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
"path": item.id,
|
|
152
|
+
"name": item.name,
|
|
153
|
+
"metadata": {
|
|
154
|
+
"mime_type": item.mime_type,
|
|
155
|
+
"md5_checksum": item.md5_checksum,
|
|
156
|
+
"modified_time": item.modified_time,
|
|
157
|
+
"size": item.size,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
for item in self.list_children(self.folder_id)
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
def get_file_content(self, file_path: str) -> bytes:
|
|
164
|
+
"""Bytes of a file by id (exported first if it is a Google Docs/Sheets/Slides file)."""
|
|
165
|
+
content, _, _ = self.download(file_path)
|
|
166
|
+
return content
|
|
167
|
+
|
|
168
|
+
def upload_file(self, file_path: str, content: bytes, content_type: str = None) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Uploads to the configured folder. Drive addresses folders by id, so
|
|
171
|
+
`file_path` is used as the file name.
|
|
172
|
+
"""
|
|
173
|
+
file_metadata = {'name': file_path, 'parents': [self.folder_id]}
|
|
174
|
+
media = MediaIoBaseUpload(io.BytesIO(content), mimetype=content_type, resumable=True)
|
|
175
|
+
self.drive_service.files().create(body=file_metadata, media_body=media, fields='id').execute()
|
|
176
|
+
|
|
177
|
+
def delete_file(self, file_path: str) -> None:
|
|
178
|
+
"""Deletes by file id; a file that is already gone is not an error."""
|
|
179
|
+
try:
|
|
180
|
+
self.drive_service.files().delete(fileId=file_path).execute()
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def generate_presigned_url(self, file_path: str, expiration: int = 3600) -> str:
|
|
185
|
+
raise NotImplementedError("not implemented yet")
|
|
186
|
+
|
|
187
|
+
# --- Reading a folder tree ---------------------------------------------
|
|
188
|
+
|
|
189
|
+
def get_file(self, file_id: str) -> DriveFile | None:
|
|
190
|
+
"""The file or folder, or None when it does not exist or is not shared with this account."""
|
|
191
|
+
try:
|
|
192
|
+
payload = self.drive_service.files().get(
|
|
193
|
+
fileId=file_id,
|
|
194
|
+
fields=FILE_FIELDS,
|
|
195
|
+
supportsAllDrives=True,
|
|
196
|
+
).execute()
|
|
197
|
+
except HttpError as exc:
|
|
198
|
+
if getattr(exc, "status_code", None) in (403, 404) or getattr(getattr(exc, "resp", None), "status", None) in (403, 404):
|
|
199
|
+
return None
|
|
200
|
+
raise
|
|
201
|
+
return DriveFile.from_api(payload)
|
|
202
|
+
|
|
203
|
+
def list_children(self, folder_id: str) -> Iterator[DriveFile]:
|
|
204
|
+
"""Direct children of a folder (files, folders and shortcuts), every page."""
|
|
205
|
+
page_token = None
|
|
206
|
+
while True:
|
|
207
|
+
response = self.drive_service.files().list(
|
|
208
|
+
q=f"'{folder_id}' in parents and trashed=false",
|
|
209
|
+
fields=f"nextPageToken, files({FILE_FIELDS})",
|
|
210
|
+
pageSize=self.PAGE_SIZE,
|
|
211
|
+
pageToken=page_token,
|
|
212
|
+
supportsAllDrives=True,
|
|
213
|
+
includeItemsFromAllDrives=True,
|
|
214
|
+
).execute()
|
|
215
|
+
for payload in response.get("files", []):
|
|
216
|
+
yield DriveFile.from_api(payload)
|
|
217
|
+
page_token = response.get("nextPageToken")
|
|
218
|
+
if not page_token:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
def walk(self, folder_id: str | None = None) -> Iterator[DriveFile]:
|
|
222
|
+
"""
|
|
223
|
+
Every file below a folder, with its path relative to it ("Contratos/2026").
|
|
224
|
+
|
|
225
|
+
Folders are entered, never returned. Shortcuts are skipped: following one
|
|
226
|
+
would bring in a file from outside the folder that was shared.
|
|
227
|
+
"""
|
|
228
|
+
root = folder_id or self.folder_id
|
|
229
|
+
pending = [(root, "")]
|
|
230
|
+
seen = {root}
|
|
231
|
+
while pending:
|
|
232
|
+
current, path = pending.pop(0)
|
|
233
|
+
for item in self.list_children(current):
|
|
234
|
+
if item.is_folder:
|
|
235
|
+
if item.id not in seen:
|
|
236
|
+
seen.add(item.id)
|
|
237
|
+
pending.append((item.id, f"{path}/{item.name}".strip("/")))
|
|
238
|
+
continue
|
|
239
|
+
if item.is_shortcut:
|
|
240
|
+
continue
|
|
241
|
+
yield DriveFile(
|
|
242
|
+
id=item.id, name=item.name, mime_type=item.mime_type, path=path,
|
|
243
|
+
parents=item.parents, md5_checksum=item.md5_checksum,
|
|
244
|
+
modified_time=item.modified_time, size=item.size, trashed=item.trashed,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def download(self, file_id: str, mime_type: str | None = None) -> tuple[bytes, str | None, str | None]:
|
|
248
|
+
"""
|
|
249
|
+
The file's bytes, the extension to give it and their MIME type. Google
|
|
250
|
+
Docs, Sheets and Slides are exported (docx, xlsx, pdf); other Google
|
|
251
|
+
formats (forms, maps) cannot be read and raise ValueError.
|
|
252
|
+
"""
|
|
253
|
+
if mime_type is None:
|
|
254
|
+
meta = self.get_file(file_id)
|
|
255
|
+
mime_type = meta.mime_type if meta else ""
|
|
256
|
+
if str(mime_type).startswith(GOOGLE_APPS_PREFIX):
|
|
257
|
+
export = EXPORT_FORMATS.get(mime_type)
|
|
258
|
+
if not export:
|
|
259
|
+
raise ValueError(f"Google format '{mime_type}' cannot be exported")
|
|
260
|
+
export_mime, extension = export
|
|
261
|
+
request = self.drive_service.files().export_media(fileId=file_id, mimeType=export_mime)
|
|
262
|
+
return self._read(request), extension, export_mime
|
|
263
|
+
request = self.drive_service.files().get_media(fileId=file_id, supportsAllDrives=True)
|
|
264
|
+
return self._read(request), None, mime_type or None
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _read(request) -> bytes:
|
|
268
|
+
buffer = io.BytesIO()
|
|
269
|
+
downloader = MediaIoBaseDownload(buffer, request)
|
|
270
|
+
done = False
|
|
271
|
+
while not done:
|
|
272
|
+
_, done = downloader.next_chunk()
|
|
273
|
+
return buffer.getvalue()
|
|
274
|
+
|
|
275
|
+
# --- Changes ------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def get_start_page_token(self) -> str:
|
|
278
|
+
"""Where to start reading changes from: everything after now."""
|
|
279
|
+
response = self.drive_service.changes().getStartPageToken(supportsAllDrives=True).execute()
|
|
280
|
+
return str(response.get("startPageToken") or "")
|
|
281
|
+
|
|
282
|
+
def list_changes(self, page_token: str) -> DriveChangesPage:
|
|
283
|
+
"""
|
|
284
|
+
One page of what changed since `page_token`, for everything this account
|
|
285
|
+
can see. When `new_start_page_token` is set the caller is up to date and
|
|
286
|
+
stores it; otherwise it keeps reading with `next_page_token`.
|
|
287
|
+
"""
|
|
288
|
+
response = self.drive_service.changes().list(
|
|
289
|
+
pageToken=page_token,
|
|
290
|
+
pageSize=self.PAGE_SIZE,
|
|
291
|
+
fields=f"nextPageToken, newStartPageToken, changes(fileId, removed, file({FILE_FIELDS}))",
|
|
292
|
+
supportsAllDrives=True,
|
|
293
|
+
includeItemsFromAllDrives=True,
|
|
294
|
+
spaces="drive",
|
|
295
|
+
).execute()
|
|
296
|
+
changes = [
|
|
297
|
+
DriveChange(
|
|
298
|
+
file_id=str(change.get("fileId") or ""),
|
|
299
|
+
removed=bool(change.get("removed")),
|
|
300
|
+
file=DriveFile.from_api(change["file"]) if isinstance(change.get("file"), dict) else None,
|
|
301
|
+
)
|
|
302
|
+
for change in response.get("changes", [])
|
|
303
|
+
]
|
|
304
|
+
return DriveChangesPage(
|
|
305
|
+
changes=changes,
|
|
306
|
+
next_page_token=response.get("nextPageToken"),
|
|
307
|
+
new_start_page_token=response.get("newStartPageToken"),
|
|
308
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from injector import inject
|
|
9
|
+
from sqlalchemy import desc
|
|
10
|
+
|
|
11
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
12
|
+
from iatoolkit.repositories.models import (
|
|
13
|
+
LLMQuery,
|
|
14
|
+
MemoryQuestion,
|
|
15
|
+
MemoryQuestionCandidate,
|
|
16
|
+
MemoryQuestionScanCursor,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MemoryQuestionRepo:
|
|
21
|
+
"""Memoria's unanswered questions (BL-005, Fase 1) and what the scan reads."""
|
|
22
|
+
|
|
23
|
+
@inject
|
|
24
|
+
def __init__(self, db_manager: DatabaseManager):
|
|
25
|
+
self.session = db_manager.get_session()
|
|
26
|
+
|
|
27
|
+
def commit(self) -> None:
|
|
28
|
+
try:
|
|
29
|
+
self.session.commit()
|
|
30
|
+
except Exception:
|
|
31
|
+
self.session.rollback()
|
|
32
|
+
raise
|
|
33
|
+
|
|
34
|
+
def rollback(self) -> None:
|
|
35
|
+
self.session.rollback()
|
|
36
|
+
|
|
37
|
+
def add(self, entity):
|
|
38
|
+
self.session.add(entity)
|
|
39
|
+
return entity
|
|
40
|
+
|
|
41
|
+
# --- questions -------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def list_questions(self, company_id: int, status: str | None = MemoryQuestion.STATUS_OPEN,
|
|
44
|
+
limit: int = 200) -> list[MemoryQuestion]:
|
|
45
|
+
query = self.session.query(MemoryQuestion).filter(MemoryQuestion.company_id == company_id)
|
|
46
|
+
if status:
|
|
47
|
+
query = query.filter(MemoryQuestion.status == status)
|
|
48
|
+
return (
|
|
49
|
+
query.order_by(desc(MemoryQuestion.times_asked), desc(MemoryQuestion.last_asked_at), desc(MemoryQuestion.id))
|
|
50
|
+
.limit(limit)
|
|
51
|
+
.all()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def get_question(self, company_id: int, question_id: int) -> MemoryQuestion | None:
|
|
55
|
+
return (
|
|
56
|
+
self.session.query(MemoryQuestion)
|
|
57
|
+
.filter(MemoryQuestion.company_id == company_id, MemoryQuestion.id == question_id)
|
|
58
|
+
.first()
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def count_open(self, company_id: int) -> int:
|
|
62
|
+
return (
|
|
63
|
+
self.session.query(MemoryQuestion)
|
|
64
|
+
.filter(MemoryQuestion.company_id == company_id, MemoryQuestion.status == MemoryQuestion.STATUS_OPEN)
|
|
65
|
+
.count()
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# --- what the scan reads ---------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def get_cursor(self, company_id: int) -> MemoryQuestionScanCursor:
|
|
71
|
+
cursor = (
|
|
72
|
+
self.session.query(MemoryQuestionScanCursor)
|
|
73
|
+
.filter(MemoryQuestionScanCursor.company_id == company_id)
|
|
74
|
+
.first()
|
|
75
|
+
)
|
|
76
|
+
if cursor is None:
|
|
77
|
+
cursor = MemoryQuestionScanCursor(company_id=company_id, last_query_id=0)
|
|
78
|
+
self.session.add(cursor)
|
|
79
|
+
self.commit()
|
|
80
|
+
return cursor
|
|
81
|
+
|
|
82
|
+
def list_queries_after(self, company_id: int, last_query_id: int, limit: int) -> list[LLMQuery]:
|
|
83
|
+
return (
|
|
84
|
+
self.session.query(LLMQuery)
|
|
85
|
+
.filter(LLMQuery.company_id == company_id, LLMQuery.id > int(last_query_id or 0))
|
|
86
|
+
.order_by(LLMQuery.id.asc())
|
|
87
|
+
.limit(limit)
|
|
88
|
+
.all()
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def has_queries_after(self, company_id: int, last_query_id: int) -> bool:
|
|
92
|
+
return (
|
|
93
|
+
self.session.query(LLMQuery.id)
|
|
94
|
+
.filter(LLMQuery.company_id == company_id, LLMQuery.id > int(last_query_id or 0))
|
|
95
|
+
.first()
|
|
96
|
+
is not None
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def find_cursor(self, company_id: int) -> MemoryQuestionScanCursor | None:
|
|
100
|
+
return (
|
|
101
|
+
self.session.query(MemoryQuestionScanCursor)
|
|
102
|
+
.filter(MemoryQuestionScanCursor.company_id == company_id)
|
|
103
|
+
.first()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def list_pending_candidates(self, company_id: int, limit: int) -> list[MemoryQuestionCandidate]:
|
|
107
|
+
return (
|
|
108
|
+
self.session.query(MemoryQuestionCandidate)
|
|
109
|
+
.filter(MemoryQuestionCandidate.company_id == company_id, MemoryQuestionCandidate.processed_at.is_(None))
|
|
110
|
+
.order_by(MemoryQuestionCandidate.id.asc())
|
|
111
|
+
.limit(limit)
|
|
112
|
+
.all()
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def has_pending_candidates(self, company_id: int) -> bool:
|
|
116
|
+
return (
|
|
117
|
+
self.session.query(MemoryQuestionCandidate.id)
|
|
118
|
+
.filter(MemoryQuestionCandidate.company_id == company_id, MemoryQuestionCandidate.processed_at.is_(None))
|
|
119
|
+
.first()
|
|
120
|
+
is not None
|
|
121
|
+
)
|
|
@@ -564,6 +564,68 @@ class MemoryWikiPageSource(Base):
|
|
|
564
564
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
565
565
|
|
|
566
566
|
|
|
567
|
+
class MemoryQuestion(Base):
|
|
568
|
+
"""
|
|
569
|
+
A question about the company that the AI could not answer (BL-005, Fase 1).
|
|
570
|
+
|
|
571
|
+
Written neutrally and grouped: the same question asked again only adds one
|
|
572
|
+
to `times_asked`. Who asked is never kept; the count is enough to rank them.
|
|
573
|
+
Answering it saves a normal Memoria capture, attributed to whoever answered.
|
|
574
|
+
"""
|
|
575
|
+
__tablename__ = "iat_memory_questions"
|
|
576
|
+
|
|
577
|
+
STATUS_OPEN = "open"
|
|
578
|
+
STATUS_ANSWERED = "answered"
|
|
579
|
+
STATUS_DISMISSED = "dismissed"
|
|
580
|
+
|
|
581
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
582
|
+
company_id = Column(Integer, ForeignKey(f"{ORM_SCHEMA}.iat_companies.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
583
|
+
question = Column(Text, nullable=False)
|
|
584
|
+
topic = Column(String(200), nullable=True)
|
|
585
|
+
times_asked = Column(Integer, nullable=False, default=1)
|
|
586
|
+
# How many times each channel brought it: {"chat": 3, "mcp": 1}.
|
|
587
|
+
channels = Column(JSON_NATIVE, nullable=True)
|
|
588
|
+
first_asked_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
589
|
+
last_asked_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
590
|
+
status = Column(String(16), nullable=False, default=STATUS_OPEN, index=True)
|
|
591
|
+
answer_text = Column(Text, nullable=True)
|
|
592
|
+
answer_capture_id = Column(Integer, ForeignKey(f"{ORM_SCHEMA}.iat_memory_captures.id", ondelete="SET NULL"), nullable=True)
|
|
593
|
+
resolved_by = Column(String, nullable=True)
|
|
594
|
+
resolved_at = Column(DateTime, nullable=True)
|
|
595
|
+
created_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
596
|
+
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False)
|
|
597
|
+
|
|
598
|
+
def to_dict(self):
|
|
599
|
+
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class MemoryQuestionCandidate(Base):
|
|
603
|
+
"""
|
|
604
|
+
A Memoria search that found nothing outside the chat (MCP: Claude, ChatGPT),
|
|
605
|
+
whose questions never reach `iat_queries`. The next scan classifies it with
|
|
606
|
+
the chat's turns. Only the text is kept, not who searched.
|
|
607
|
+
"""
|
|
608
|
+
__tablename__ = "iat_memory_question_candidates"
|
|
609
|
+
|
|
610
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
611
|
+
company_id = Column(Integer, ForeignKey(f"{ORM_SCHEMA}.iat_companies.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
612
|
+
query_text = Column(Text, nullable=False)
|
|
613
|
+
source = Column(String(16), nullable=False, default="mcp")
|
|
614
|
+
processed_at = Column(DateTime, nullable=True, index=True)
|
|
615
|
+
created_at = Column(DateTime, default=datetime.now, nullable=False)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class MemoryQuestionScanCursor(Base):
|
|
619
|
+
"""Up to which `iat_queries.id` a company's turns were already reviewed for unanswered questions."""
|
|
620
|
+
__tablename__ = "iat_memory_question_scan_cursors"
|
|
621
|
+
|
|
622
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
623
|
+
company_id = Column(Integer, ForeignKey(f"{ORM_SCHEMA}.iat_companies.id", ondelete="CASCADE"), nullable=False, unique=True)
|
|
624
|
+
last_query_id = Column(Integer, nullable=False, default=0)
|
|
625
|
+
last_scan_at = Column(DateTime, nullable=True)
|
|
626
|
+
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False)
|
|
627
|
+
|
|
628
|
+
|
|
567
629
|
#: request_source of the iat_queries row that records a context initialization:
|
|
568
630
|
#: the company context sent once to start a server-side conversation. Its tokens
|
|
569
631
|
#: are billed, but it is not a question, so conversation readers skip it.
|
|
@@ -481,8 +481,10 @@ class ContextBuilderService:
|
|
|
481
481
|
lines = [
|
|
482
482
|
"## Colecciones documentales disponibles",
|
|
483
483
|
"Usa `iat_document_search` cuando documentos internos de la empresa puedan ayudar a responder al usuario.",
|
|
484
|
+
"Usa `iat_document_list` para saber qué documentos hay en una colección y `iat_document_read` para leer uno completo.",
|
|
484
485
|
"### Colecciones",
|
|
485
486
|
]
|
|
487
|
+
header_lines = len(lines)
|
|
486
488
|
|
|
487
489
|
for name, description in selected_entries[:20]:
|
|
488
490
|
if description:
|
|
@@ -490,7 +492,7 @@ class ContextBuilderService:
|
|
|
490
492
|
else:
|
|
491
493
|
lines.append(f"- {name}")
|
|
492
494
|
|
|
493
|
-
if len(lines) <=
|
|
495
|
+
if len(lines) <= header_lines:
|
|
494
496
|
return ""
|
|
495
497
|
|
|
496
498
|
lines.append(
|