iatoolkit 2.77.1__tar.gz → 2.77.3__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.
Files changed (286) hide show
  1. {iatoolkit-2.77.1/src/iatoolkit.egg-info → iatoolkit-2.77.3}/PKG-INFO +1 -1
  2. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/__init__.py +1 -1
  3. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/openrouter_decision_client.py +9 -1
  4. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/llm_query_repo.py +4 -0
  5. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/configuration_service.py +6 -0
  6. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/context_builder_service.py +16 -0
  7. iatoolkit-2.77.3/src/iatoolkit/services/decision_attachments.py +248 -0
  8. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/decision_service.py +53 -2
  9. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/llm_client_service.py +11 -1
  10. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/query_service.py +24 -3
  11. {iatoolkit-2.77.1 → iatoolkit-2.77.3/src/iatoolkit.egg-info}/PKG-INFO +1 -1
  12. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit.egg-info/SOURCES.txt +1 -0
  13. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/LICENSE +0 -0
  14. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/LICENSE_COMMUNITY.md +0 -0
  15. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/pyproject.toml +0 -0
  16. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/readme.md +0 -0
  17. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/setup.cfg +0 -0
  18. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/base_company.py +0 -0
  19. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/cli_commands.py +0 -0
  20. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/__init__.py +0 -0
  21. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/access.py +0 -0
  22. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/bridge_validation.py +0 -0
  23. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/company_paths.py +0 -0
  24. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/decision_types.py +0 -0
  25. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/exceptions.py +0 -0
  26. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/__init__.py +0 -0
  27. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/asset_storage.py +0 -0
  28. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/database_provider.py +0 -0
  29. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/history_compaction_trigger.py +0 -0
  30. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/memory_compilation_trigger.py +0 -0
  31. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/secret_provider.py +0 -0
  32. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/signup_policy_resolver.py +0 -0
  33. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/interfaces/web_search_provider.py +0 -0
  34. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/memory_tools.py +0 -0
  35. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/model_registry.py +0 -0
  36. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/routes.py +0 -0
  37. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/search_terms.py +0 -0
  38. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/secret_resolver.py +0 -0
  39. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/session_manager.py +0 -0
  40. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/text_splitter.py +0 -0
  41. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/url_safety.py +0 -0
  42. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/common/util.py +0 -0
  43. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/company_registry.py +0 -0
  44. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/__init__.py +0 -0
  45. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/llm_capabilities.yaml +0 -0
  46. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/history_compaction.prompt +0 -0
  47. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/memory_compiler.prompt +0 -0
  48. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/memory_correction.prompt +0 -0
  49. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/memory_document_ingest.prompt +0 -0
  50. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/memory_image_ingest.prompt +0 -0
  51. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/memory_meeting_ingest.prompt +0 -0
  52. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents/memory_question_review.prompt +0 -0
  53. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_agents_pack.yaml +0 -0
  54. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/channel_business_identity.prompt +0 -0
  55. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/channel_conversation_continuity.prompt +0 -0
  56. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/channel_response_style.prompt +0 -0
  57. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/chat_state_rules.prompt +0 -0
  58. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/chat_user_profile.prompt +0 -0
  59. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/core_identity.prompt +0 -0
  60. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/email_output.prompt +0 -0
  61. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/file_download_output.prompt +0 -0
  62. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/format_styles.prompt +0 -0
  63. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/html_structures.prompt +0 -0
  64. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/links_documents.prompt +0 -0
  65. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/memory_usage.prompt +0 -0
  66. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/multimodal_basics.prompt +0 -0
  67. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/output_basics.prompt +0 -0
  68. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/presentation_formatting.prompt +0 -0
  69. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/query_main.prompt +0 -0
  70. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_aggregation_scope.prompt +0 -0
  71. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_casting.prompt +0 -0
  72. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_core.prompt +0 -0
  73. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_jsonb.prompt +0 -0
  74. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_mysql_casting.prompt +0 -0
  75. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_mysql_json.prompt +0 -0
  76. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_redshift_basics.prompt +0 -0
  77. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/sql_redshift_types.prompt +0 -0
  78. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts/tool_html_passthrough.prompt +0 -0
  79. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_prompts_pack.yaml +0 -0
  80. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/config/system_tools_pack.yaml +0 -0
  81. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/core.py +0 -0
  82. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/__init__.py +0 -0
  83. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/brevo_mail_app.py +0 -0
  84. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/call_service.py +0 -0
  85. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/__init__.py +0 -0
  86. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
  87. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
  88. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
  89. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
  90. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/local_file_connector.py +0 -0
  91. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
  92. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/google_auth_client.py +0 -0
  93. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/google_chat_app.py +0 -0
  94. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/inference_embeddings_client.py +0 -0
  95. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/inference_handler.py +0 -0
  96. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/jina_embeddings_client.py +0 -0
  97. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_gateway_resolver.py +0 -0
  98. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/__init__.py +0 -0
  99. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/anthropic_adapter.py +0 -0
  100. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/deepseek_adapter.py +0 -0
  101. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/gemini_adapter.py +0 -0
  102. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/openai_adapter.py +0 -0
  103. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/openai_compatible_chat_adapter.py +0 -0
  104. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_providers/openrouter_adapter.py +0 -0
  105. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_proxy.py +0 -0
  106. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/llm_response.py +0 -0
  107. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/infra/redis_session_manager.py +0 -0
  108. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/locales/en.yaml +0 -0
  109. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/locales/es.yaml +0 -0
  110. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/__init__.py +0 -0
  111. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/advisory_lock.py +0 -0
  112. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/api_key_repo.py +0 -0
  113. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/bridge_repo.py +0 -0
  114. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/database_manager.py +0 -0
  115. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/document_repo.py +0 -0
  116. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/env_secret_provider.py +0 -0
  117. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/filesystem_asset_repository.py +0 -0
  118. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/knowledge_wiki_repo.py +0 -0
  119. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/mcp_token_repo.py +0 -0
  120. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/memory_question_repo.py +0 -0
  121. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/memory_repo.py +0 -0
  122. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/memory_usage_repo.py +0 -0
  123. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/models.py +0 -0
  124. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/profile_repo.py +0 -0
  125. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/prompt_resource_repo.py +0 -0
  126. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/sql_dataset_repo.py +0 -0
  127. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/sql_source_repo.py +0 -0
  128. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/repositories/vs_repo.py +0 -0
  129. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/runtime_logging.py +0 -0
  130. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/__init__.py +0 -0
  131. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/api_key_service.py +0 -0
  132. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/attachment_policy_service.py +0 -0
  133. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/auth_service.py +0 -0
  134. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/branding_service.py +0 -0
  135. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/company_context_service.py +0 -0
  136. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/dispatcher_service.py +0 -0
  137. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/embedding_service.py +0 -0
  138. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/excel_service.py +0 -0
  139. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/file_processor_service.py +0 -0
  140. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/history_compaction_service.py +0 -0
  141. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/history_manager_service.py +0 -0
  142. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/http_tool_service.py +0 -0
  143. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/i18n_service.py +0 -0
  144. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/inference_service.py +0 -0
  145. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/jwt_service.py +0 -0
  146. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/knowledge_base_service.py +0 -0
  147. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/language_service.py +0 -0
  148. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/license_service.py +0 -0
  149. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/mail_service.py +0 -0
  150. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/markdown_wiki_service.py +0 -0
  151. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/mcp_token_service.py +0 -0
  152. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_compiler_service.py +0 -0
  153. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_lookup_policy_service.py +0 -0
  154. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_question_service.py +0 -0
  155. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_service.py +0 -0
  156. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_sources.py +0 -0
  157. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_usage_service.py +0 -0
  158. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/memory_wiki_service.py +0 -0
  159. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/noop_history_compaction_trigger.py +0 -0
  160. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/noop_memory_compilation_trigger.py +0 -0
  161. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/operator_identity_service.py +0 -0
  162. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/__init__.py +0 -0
  163. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/contracts.py +0 -0
  164. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/image_normalizer.py +0 -0
  165. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/parsing_service.py +0 -0
  166. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/pdf_ocr_detection.py +0 -0
  167. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/provider_factory.py +0 -0
  168. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/provider_resolver.py +0 -0
  169. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/providers/__init__.py +0 -0
  170. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/providers/basic_provider.py +0 -0
  171. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/providers/docling_provider.py +0 -0
  172. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/parsers/validator.py +0 -0
  173. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/pdf_service.py +0 -0
  174. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/profile_service.py +0 -0
  175. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/prompt_resource_service.py +0 -0
  176. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/prompt_service.py +0 -0
  177. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/resource_access_scope.py +0 -0
  178. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/signup_guard_service.py +0 -0
  179. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/signup_policy_resolver.py +0 -0
  180. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/sql_catalog_service.py +0 -0
  181. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/sql_dataset_service.py +0 -0
  182. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/sql_service.py +0 -0
  183. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/sql_source_service.py +0 -0
  184. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/storage_service.py +0 -0
  185. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/structured_output_service.py +0 -0
  186. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/system_agent_catalog.py +0 -0
  187. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/system_agent_runner.py +0 -0
  188. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/system_prompt_catalog.py +0 -0
  189. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/system_tools.py +0 -0
  190. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/telemetry_service.py +0 -0
  191. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/tenant_wiki_service.py +0 -0
  192. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/tool_output_contract.py +0 -0
  193. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/tool_service.py +0 -0
  194. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/user_feedback_service.py +0 -0
  195. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/user_session_context_service.py +0 -0
  196. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/visual_kb_service.py +0 -0
  197. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/visual_tool_service.py +0 -0
  198. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/warmup_service.py +0 -0
  199. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/web_search/__init__.py +0 -0
  200. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/web_search/provider_factory.py +0 -0
  201. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/web_search/providers/__init__.py +0 -0
  202. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/web_search/providers/brave_provider.py +0 -0
  203. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/web_search_service.py +0 -0
  204. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/services/wiki_page_index_service.py +0 -0
  205. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/images/favicon.ico +0 -0
  206. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/images/favicon.png +0 -0
  207. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/images/fernando.jpeg +0 -0
  208. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/images/iatoolkit_core.png +0 -0
  209. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/images/iatoolkit_logo.png +0 -0
  210. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_feedback_button.js +0 -0
  211. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_filepond.js +0 -0
  212. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_header_navigation.js +0 -0
  213. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_help_content.js +0 -0
  214. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_history_button.js +0 -0
  215. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_logout_button.js +0 -0
  216. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_main.js +0 -0
  217. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_memory_button.js +0 -0
  218. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_model_selector.js +0 -0
  219. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_onboarding_button.js +0 -0
  220. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/js/chat_prompt_manager.js +0 -0
  221. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/account.css +0 -0
  222. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
  223. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/chat_modal.css +0 -0
  224. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/chat_public.css +0 -0
  225. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/documents.css +0 -0
  226. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/landing_page.css +0 -0
  227. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/llm_output.css +0 -0
  228. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/onboarding.css +0 -0
  229. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/static/styles/wiki_page.css +0 -0
  230. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/_chat_header.html +0 -0
  231. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/_company_header.html +0 -0
  232. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/_login_widget.html +0 -0
  233. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/account.html +0 -0
  234. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/base.html +0 -0
  235. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/change_password.html +0 -0
  236. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/chat.html +0 -0
  237. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/chat_memory_modal.html +0 -0
  238. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/chat_modals.html +0 -0
  239. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/error.html +0 -0
  240. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/forgot_password.html +0 -0
  241. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/home_hosted_default.html +0 -0
  242. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/login_global.html +0 -0
  243. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/memory/wiki_schema.md +0 -0
  244. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/pdf/base.html +0 -0
  245. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/pdf/letter.html +0 -0
  246. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/pdf/report.html +0 -0
  247. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/pdf/simple.html +0 -0
  248. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/signup.html +0 -0
  249. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/templates/wiki_page.html +0 -0
  250. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/__init__.py +0 -0
  251. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/account_view.py +0 -0
  252. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/api_key_api_view.py +0 -0
  253. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/base_login_view.py +0 -0
  254. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/categories_api_view.py +0 -0
  255. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/change_password_view.py +0 -0
  256. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/chat_context_preview_api_view.py +0 -0
  257. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/chat_view.py +0 -0
  258. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/configuration_api_view.py +0 -0
  259. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/connectors_api_view.py +0 -0
  260. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/embedding_api_view.py +0 -0
  261. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/forgot_password_view.py +0 -0
  262. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/help_content_api_view.py +0 -0
  263. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/history_api_view.py +0 -0
  264. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/home_view.py +0 -0
  265. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/init_context_api_view.py +0 -0
  266. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/llmquery_api_view.py +0 -0
  267. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/load_document_api_view.py +0 -0
  268. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/login_view.py +0 -0
  269. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/logout_api_view.py +0 -0
  270. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/memory_api_view.py +0 -0
  271. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/profile_api_view.py +0 -0
  272. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/prompt_api_view.py +0 -0
  273. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/prompt_context_preview_api_view.py +0 -0
  274. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/prompt_resource_api_view.py +0 -0
  275. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/rag_api_view.py +0 -0
  276. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/root_redirect_view.py +0 -0
  277. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/signup_view.py +0 -0
  278. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/static_page_view.py +0 -0
  279. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/tool_api_view.py +0 -0
  280. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
  281. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/users_api_view.py +0 -0
  282. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/verify_user_view.py +0 -0
  283. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit/views/wiki_page_view.py +0 -0
  284. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
  285. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit.egg-info/requires.txt +0 -0
  286. {iatoolkit-2.77.1 → iatoolkit-2.77.3}/src/iatoolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 2.77.1
3
+ Version: 2.77.3
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- __version__ = "2.77.1"
6
+ __version__ = "2.77.3"
7
7
 
8
8
  # Expose main classes and functions at the top level of the package
9
9
 
@@ -66,12 +66,15 @@ class OpenRouterDecisionClient:
66
66
  read_timeout_seconds: float = 60.0,
67
67
  retry_attempts: int = DEFAULT_RETRY_ATTEMPTS,
68
68
  user: str | None = None,
69
+ assets: dict[str, dict[str, str]] | None = None,
69
70
  ) -> DecisionResult:
70
71
  payload: dict[str, Any] = {
71
72
  "model": model,
72
73
  "state": state,
73
74
  "questions": {name: question.to_wire() for name, question in questions.items()},
74
75
  }
76
+ if assets:
77
+ payload["assets"] = assets
75
78
  if provider_policy:
76
79
  payload["provider"] = dict(provider_policy)
77
80
  if user:
@@ -109,7 +112,12 @@ class OpenRouterDecisionClient:
109
112
  return DecisionResult.from_response(body, requested_model=model, latency_ms=latency_ms)
110
113
 
111
114
  message = self._error_message(response)
112
- if response.status_code in RETRYABLE_STATUS and attempt < attempts:
115
+ # With images, a 5xx is how an oversized request answers (measured: a
116
+ # bare 500); sending it again only repeats it. Rate limits still retry.
117
+ retryable = response.status_code in RETRYABLE_STATUS and (
118
+ not assets or response.status_code == 429
119
+ )
120
+ if retryable and attempt < attempts:
113
121
  logging.warning(
114
122
  "Decision model '%s' answered %s (attempt %s/%s): %s",
115
123
  model, response.status_code, attempt, attempts, message,
@@ -14,6 +14,7 @@ from iatoolkit.repositories.models import (
14
14
  from injector import inject
15
15
  from iatoolkit.repositories.database_manager import DatabaseManager
16
16
  from sqlalchemy import or_, and_
17
+ from sqlalchemy.orm.attributes import flag_modified
17
18
  from iatoolkit.repositories.models import CONTEXT_INIT_REQUEST_SOURCE
18
19
  from typing import List
19
20
 
@@ -233,6 +234,9 @@ class LLMQueryRepo:
233
234
  or dict(getattr(prompt, "runtime_policy", None) or {})
234
235
  )
235
236
  prompt.decision_spec = getattr(new_prompt, "decision_spec", None)
237
+ # The order of the questions is part of the agent, and dicts compare
238
+ # equal regardless of it: without this a reorder is never written.
239
+ flag_modified(prompt, "decision_spec")
236
240
  else:
237
241
  if new_prompt.active is None:
238
242
  new_prompt.active = True
@@ -658,6 +658,12 @@ class ConfigurationService:
658
658
  else:
659
659
  if decisions_cfg.get("provider") is not None and not isinstance(decisions_cfg.get("provider"), dict):
660
660
  add_error("llm.decisions.provider", "Must be a dictionary (e.g. {zdr: true}).")
661
+ raw_max_images = decisions_cfg.get("max_images")
662
+ if raw_max_images is not None and (
663
+ isinstance(raw_max_images, bool) or not isinstance(raw_max_images, int)
664
+ or not 1 <= raw_max_images <= 20
665
+ ):
666
+ add_error("llm.decisions.max_images", "Must be an integer between 1 and 20.")
661
667
  raw_path = decisions_cfg.get("path")
662
668
  if raw_path is not None and (not isinstance(raw_path, str) or not raw_path.strip().startswith("/")):
663
669
  add_error("llm.decisions.path", "Must be a path starting with '/'.")
@@ -577,6 +577,22 @@ class ContextBuilderService:
577
577
  images=[],
578
578
  ) or "").strip()
579
579
 
580
+ def prepare_decision_attachments(self, company: Company, files: list, *, mode: str | None,
581
+ max_images: int):
582
+ """A decision agent's attachments (BL-008, Etapa 2): images to send and text for the case."""
583
+ from iatoolkit.services.decision_attachments import prepare_decision_attachments
584
+
585
+ company_short_name = str(getattr(company, "short_name", "") or "default")
586
+ return prepare_decision_attachments(
587
+ files,
588
+ mode=mode,
589
+ max_images=max_images,
590
+ decode=self.util.normalize_base64_payload,
591
+ extract_text=lambda filename, content: self.parsing_service.extract_text_for_context(
592
+ filename=filename, content=content, company_short_name=company_short_name,
593
+ ),
594
+ )
595
+
580
596
  def build_user_turn_prompt(self,
581
597
  company: Company,
582
598
  user_identifier: str,
@@ -0,0 +1,248 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ """Attachments of a decision agent (BL-008, Etapa 2): images and PDFs.
7
+
8
+ Decision models take images only as PNG or JPEG `assets`, and each one limits
9
+ what it accepts: Jev refuses anything over ~1568 px on its longest side, and
10
+ every model tested answered a bare 500 to a 4 MB image. A PDF is not an asset
11
+ at all. So nothing goes out as it came in:
12
+
13
+ - every image, and every PDF page sent as an image, is normalized to a JPEG of
14
+ at most 1568 px, which fits every model, keeps the request small, lowers the
15
+ cost (image tokens grow with resolution) and accepts any format Pillow reads;
16
+ - a PDF's text, when the agent asks for it, joins the case as text, through
17
+ the same extractor the chat agents use.
18
+
19
+ What reaches the model is decided by the agent's attachment mode, the same
20
+ setting chat agents have:
21
+
22
+ | mode | image | PDF |
23
+ |-------------------------|---------------|-----------------------------|
24
+ | `extracted_only` | refused | its text joins the case |
25
+ | `native_only` | sent as image | its pages are sent as images|
26
+ | `native_plus_extracted` | sent as image | text and pages |
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import hashlib
32
+ import io
33
+ from dataclasses import dataclass, field
34
+ from typing import Any, Callable
35
+
36
+ from iatoolkit.common.exceptions import IAToolkitException
37
+
38
+ MODE_EXTRACTED = "extracted_only"
39
+ MODE_NATIVE = "native_only"
40
+ MODE_BOTH = "native_plus_extracted"
41
+ MODES = (MODE_EXTRACTED, MODE_NATIVE, MODE_BOTH)
42
+
43
+ #: Longest side of every image sent. Jev's limit, measured (1568 passes, 2048
44
+ #: fails); every other model tested accepts it.
45
+ MAX_IMAGE_SIDE = 1568
46
+ JPEG_QUALITY = 85
47
+ #: Bytes of one image after normalization. 2.7 MB passed and 4 MB failed with a
48
+ #: 500; a 1568 px JPEG is far below this, so it only guards odd inputs.
49
+ MAX_IMAGE_BYTES = 2_500_000
50
+ DEFAULT_MAX_IMAGES = 5
51
+
52
+ _IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff", ".heic", ".heif")
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class DecisionImage:
57
+ """One image as sent: a normalized JPEG, named after where it came from."""
58
+ name: str
59
+ data: bytes
60
+ source: str
61
+ page: int | None = None
62
+
63
+ @property
64
+ def sha256(self) -> str:
65
+ return hashlib.sha256(self.data).hexdigest()
66
+
67
+
68
+ @dataclass
69
+ class PreparedAttachments:
70
+ images: list[DecisionImage] = field(default_factory=list)
71
+ #: Extracted text, ready to follow the rendered case.
72
+ text: str = ""
73
+ #: What was received and what was made of it, for the record (never the bytes).
74
+ manifest: list[dict[str, Any]] = field(default_factory=list)
75
+
76
+
77
+ def _refuse(message: str) -> IAToolkitException:
78
+ return IAToolkitException(IAToolkitException.ErrorType.INVALID_PARAMETER, message)
79
+
80
+
81
+ def normalize_image(data: bytes, name: str = "image") -> bytes:
82
+ """Any image Pillow reads, as a JPEG of at most MAX_IMAGE_SIDE px."""
83
+ from PIL import Image, ImageOps
84
+
85
+ try:
86
+ image = Image.open(io.BytesIO(data))
87
+ image.load()
88
+ except Exception as exc:
89
+ raise _refuse(f"The attachment '{name}' is not an image this platform can read ({exc}).") from exc
90
+
91
+ image = ImageOps.exif_transpose(image)
92
+ if image.mode in ("RGBA", "LA", "P"):
93
+ # JPEG has no transparency: flatten on white, as the page it came from.
94
+ rgba = image.convert("RGBA")
95
+ background = Image.new("RGB", rgba.size, "white")
96
+ background.paste(rgba, mask=rgba.split()[-1])
97
+ image = background
98
+ elif image.mode != "RGB":
99
+ image = image.convert("RGB")
100
+ image.thumbnail((MAX_IMAGE_SIDE, MAX_IMAGE_SIDE))
101
+
102
+ quality = JPEG_QUALITY
103
+ while True:
104
+ buffer = io.BytesIO()
105
+ image.save(buffer, "JPEG", quality=quality, optimize=True)
106
+ encoded = buffer.getvalue()
107
+ if len(encoded) <= MAX_IMAGE_BYTES or quality <= 40:
108
+ break
109
+ quality -= 15
110
+ if len(encoded) > MAX_IMAGE_BYTES:
111
+ raise _refuse(f"The image '{name}' is still over {MAX_IMAGE_BYTES // 1_000_000} MB after resizing.")
112
+ return encoded
113
+
114
+
115
+ def render_pdf_pages(data: bytes, name: str) -> list[bytes]:
116
+ """Every page of a PDF as a normalized JPEG."""
117
+ import fitz
118
+
119
+ try:
120
+ document = fitz.open(stream=data, filetype="pdf")
121
+ except Exception as exc:
122
+ raise _refuse(f"The PDF '{name}' could not be opened ({exc}).") from exc
123
+ pages = []
124
+ with document:
125
+ for page in document:
126
+ longest = max(page.rect.width, page.rect.height) or 1
127
+ zoom = min(MAX_IMAGE_SIDE / longest, 4.0)
128
+ pixmap = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
129
+ pages.append(normalize_image(pixmap.tobytes("png"), name))
130
+ return pages
131
+
132
+
133
+ def pdf_page_count(data: bytes) -> int:
134
+ import fitz
135
+
136
+ with fitz.open(stream=data, filetype="pdf") as document:
137
+ return document.page_count
138
+
139
+
140
+ def _is_image(filename: str) -> bool:
141
+ return filename.lower().endswith(_IMAGE_EXTENSIONS)
142
+
143
+
144
+ def _is_pdf(filename: str) -> bool:
145
+ return filename.lower().endswith(".pdf")
146
+
147
+
148
+ def prepare_decision_attachments(
149
+ files: list[dict] | None,
150
+ *,
151
+ mode: str | None,
152
+ max_images: int = DEFAULT_MAX_IMAGES,
153
+ decode: Callable[[Any], bytes],
154
+ extract_text: Callable[[str, bytes], str],
155
+ ) -> PreparedAttachments:
156
+ """What a decision agent sends for its attachments, or an error that says why not.
157
+
158
+ `decode` turns a file's payload (base64, possibly a data URL) into bytes;
159
+ `extract_text` is the extractor chat agents use for documents.
160
+ """
161
+ prepared = PreparedAttachments()
162
+ if not files:
163
+ return prepared
164
+ mode = str(mode or MODE_EXTRACTED).strip().lower()
165
+ if mode not in MODES:
166
+ mode = MODE_EXTRACTED
167
+ native = mode in (MODE_NATIVE, MODE_BOTH)
168
+ extracted = mode in (MODE_EXTRACTED, MODE_BOTH)
169
+ text_parts: list[str] = []
170
+
171
+ for document in files:
172
+ filename = str(document.get("file_id") or document.get("filename") or document.get("name") or "").strip()
173
+ payload = document.get("base64") or document.get("content")
174
+ if not filename or not payload:
175
+ raise _refuse("An attachment has no name or no content.")
176
+ data = decode(payload)
177
+ if isinstance(data, str):
178
+ data = data.encode("utf-8")
179
+ entry: dict[str, Any] = {
180
+ "name": filename,
181
+ "bytes": len(data),
182
+ "sha256": hashlib.sha256(data).hexdigest(),
183
+ }
184
+
185
+ if _is_image(filename):
186
+ if not native:
187
+ raise _refuse(
188
+ f"'{filename}' is an image, and this agent's attachment mode only sends extracted text. "
189
+ "Change it to native to send images."
190
+ )
191
+ _check_room(prepared, 1, max_images, filename)
192
+ prepared.images.append(DecisionImage(
193
+ name=f"img_{len(prepared.images) + 1}",
194
+ data=normalize_image(data, filename),
195
+ source=filename,
196
+ ))
197
+ entry["sent_as"] = "image"
198
+ entry["images"] = 1
199
+
200
+ elif _is_pdf(filename):
201
+ entry["pages"] = pdf_page_count(data)
202
+ sent_as = []
203
+ if extracted:
204
+ text = (extract_text(filename, data) or "").strip()
205
+ if text:
206
+ text_parts.append(f"Documento adjunto '{filename}':\n{text}")
207
+ sent_as.append("text")
208
+ elif not native:
209
+ raise _refuse(
210
+ f"The PDF '{filename}' has no text to extract (it looks scanned). "
211
+ "Change the agent's attachment mode to native to send its pages as images."
212
+ )
213
+ if native:
214
+ _check_room(prepared, entry["pages"], max_images, filename)
215
+ for page_number, page in enumerate(render_pdf_pages(data, filename), start=1):
216
+ prepared.images.append(DecisionImage(
217
+ name=f"img_{len(prepared.images) + 1}",
218
+ data=page,
219
+ source=filename,
220
+ page=page_number,
221
+ ))
222
+ sent_as.append("pages")
223
+ entry["images"] = entry["pages"]
224
+ entry["sent_as"] = "+".join(sent_as)
225
+
226
+ else:
227
+ if not extracted:
228
+ raise _refuse(
229
+ f"'{filename}' is neither an image nor a PDF; in native mode a decision agent sends only those."
230
+ )
231
+ text = (extract_text(filename, data) or "").strip()
232
+ if not text:
233
+ raise _refuse(f"No text could be extracted from '{filename}'.")
234
+ text_parts.append(f"Documento adjunto '{filename}':\n{text}")
235
+ entry["sent_as"] = "text"
236
+
237
+ prepared.manifest.append(entry)
238
+
239
+ prepared.text = "\n\n".join(text_parts)
240
+ return prepared
241
+
242
+
243
+ def _check_room(prepared: PreparedAttachments, needed: int, max_images: int, filename: str) -> None:
244
+ if len(prepared.images) + needed > max_images:
245
+ raise _refuse(
246
+ f"'{filename}' would take the images sent to {len(prepared.images) + needed}; "
247
+ f"a decision agent sends at most {max_images} (llm.decisions.max_images)."
248
+ )
@@ -12,6 +12,7 @@ endpoint but none of its adapters.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import base64
15
16
  import json
16
17
  import logging
17
18
  from typing import Any
@@ -25,6 +26,7 @@ from iatoolkit.common.decision_types import (
25
26
  parse_decision_spec,
26
27
  )
27
28
  from iatoolkit.common.exceptions import IAToolkitException
29
+ from iatoolkit.services.decision_attachments import DecisionImage
28
30
  from iatoolkit.infra.llm_proxy import LLMProxy
29
31
  from iatoolkit.infra.openrouter_decision_client import OpenRouterDecisionClient
30
32
  from iatoolkit.services.configuration_service import ConfigurationService
@@ -98,6 +100,7 @@ class DecisionService:
98
100
  decision_spec: dict,
99
101
  user_identifier: str | None = None,
100
102
  telemetry_execution: Any = None,
103
+ images: list[DecisionImage] | None = None,
101
104
  ) -> DecisionResult:
102
105
  try:
103
106
  questions = parse_decision_spec(decision_spec)
@@ -116,6 +119,10 @@ class DecisionService:
116
119
  "The case to decide on is empty: the agent template rendered no text.",
117
120
  )
118
121
  self._warn_if_too_long(company_short_name, model_key, model_config, state_text)
122
+ images = list(images or [])
123
+ if images:
124
+ self._check_takes_images(model_key, model_config)
125
+ wire_state, assets = self.build_state(model_key, state_text, images)
119
126
 
120
127
  client_config = self.llm_proxy.resolve_client_config(
121
128
  company_short_name, DECISION_PROVIDER, model_config=model_config
@@ -131,7 +138,11 @@ class DecisionService:
131
138
  name="iatoolkit.decision",
132
139
  span_type="llm",
133
140
  event={
134
- "input": {"state": state_text, "questions": {n: q.to_wire() for n, q in questions.items()}},
141
+ "input": {
142
+ "state": state_text,
143
+ "questions": {n: q.to_wire() for n, q in questions.items()},
144
+ "images": [{"name": i.name, "source": i.source, "page": i.page} for i in images],
145
+ },
135
146
  "metadata": {"model": wire_model, "company": company_short_name},
136
147
  },
137
148
  )
@@ -143,8 +154,9 @@ class DecisionService:
143
154
  api_key=client_config.get("api_key") or "",
144
155
  url=url,
145
156
  model=wire_model,
146
- state=state_text,
157
+ state=wire_state,
147
158
  questions=questions,
159
+ assets=assets,
148
160
  provider_policy=provider_policy if isinstance(provider_policy, dict) else None,
149
161
  default_headers=client_config.get("default_headers") or {},
150
162
  connect_timeout_seconds=client_config.get("connect_timeout_seconds") or 10.0,
@@ -176,6 +188,45 @@ class DecisionService:
176
188
  )
177
189
  return result
178
190
 
191
+ @staticmethod
192
+ def _check_takes_images(model: str, model_config: dict) -> None:
193
+ capabilities = model_config.get("capabilities") if isinstance(model_config.get("capabilities"), dict) else {}
194
+ modalities = {str(item).strip().lower() for item in capabilities.get("input_modalities") or []}
195
+ if "image" not in modalities:
196
+ raise IAToolkitException(
197
+ IAToolkitException.ErrorType.INVALID_PARAMETER,
198
+ f"Decision model '{model}' does not read images; choose one that does, "
199
+ "or set the agent's attachment mode to extracted text.",
200
+ )
201
+
202
+ @staticmethod
203
+ def build_state(model: str, state_text: str, images: list[DecisionImage]) -> tuple[Any, dict | None]:
204
+ """The `state` and `assets` for a case with images (measured on 9 models).
205
+
206
+ Every model that reads images takes them as whole top-level items of an
207
+ array state, referenced by their asset key: `[case, "img_1", ...]`. Only
208
+ Jev also reads a reference inside text or a nested object. ScaleDown
209
+ takes no assets at all and reads one image as a document instead.
210
+ """
211
+ if not images:
212
+ return state_text, None
213
+ if model.startswith("scaledown/"):
214
+ if len(images) > 1:
215
+ raise IAToolkitException(
216
+ IAToolkitException.ErrorType.INVALID_PARAMETER,
217
+ f"Decision model '{model}' reads one image per case; {len(images)} were sent.",
218
+ )
219
+ return {
220
+ "text": state_text,
221
+ "document": base64.b64encode(images[0].data).decode("ascii"),
222
+ "document_mime_type": "image/jpeg",
223
+ }, None
224
+ assets = {
225
+ image.name: {"data_base64": base64.b64encode(image.data).decode("ascii"), "format": "jpg"}
226
+ for image in images
227
+ }
228
+ return [state_text, *[image.name for image in images]], assets
229
+
179
230
  @staticmethod
180
231
  def _warn_if_too_long(company_short_name: str, model: str, model_config: dict, state_text: str) -> None:
181
232
  try:
@@ -146,7 +146,9 @@ class llmClient:
146
146
  task_id: Optional[int] = None,
147
147
  execution_metadata: Optional[Dict[str, Any]] = None,
148
148
  telemetry_request: Optional[Dict[str, Any]] = None,
149
- execution_trace: Optional[Dict[str, Any]] = None) -> dict:
149
+ execution_trace: Optional[Dict[str, Any]] = None,
150
+ images: Optional[list] = None,
151
+ attachments: Optional[List[Dict[str, Any]]] = None) -> dict:
150
152
  """Runs a decision agent's model (BL-008) and records it like any agent execution.
151
153
 
152
154
  Same table, same `stats` keys as a chat turn — `model`, tokens, response
@@ -159,6 +161,12 @@ class llmClient:
159
161
  telemetry_execution = self.telemetry_service.start_execution(telemetry_request)
160
162
  telemetry_execution.record_input({"state": state, "questions": (decision_spec or {}).get("questions")})
161
163
  base_stats: dict = {"model": model, "kind": "decision"}
164
+ if attachments:
165
+ # What was attached and how it was sent; never the bytes.
166
+ base_stats["attachments"] = {
167
+ "count": len(attachments),
168
+ "images_sent": len(images or []),
169
+ }
162
170
  for key, value in (execution_metadata or {}).items():
163
171
  if value is not None:
164
172
  base_stats[key] = value
@@ -171,6 +179,7 @@ class llmClient:
171
179
  decision_spec=decision_spec,
172
180
  user_identifier=user_identifier,
173
181
  telemetry_execution=telemetry_execution,
182
+ images=images,
174
183
  )
175
184
  except Exception as e:
176
185
  error_message = getattr(e, "message", None) or str(e)
@@ -226,6 +235,7 @@ class llmClient:
226
235
  "decision": result.to_dict(),
227
236
  "state": state,
228
237
  "decision_spec": decision_spec,
238
+ "attachments": list(attachments or []),
229
239
  },
230
240
  function_calls=[],
231
241
  stats=stats,
@@ -1387,9 +1387,6 @@ class QueryService:
1387
1387
  prompt_output_contract: dict,
1388
1388
  execution_trace: dict | None) -> dict:
1389
1389
  company_short_name = company.short_name
1390
- if files:
1391
- return {"error": True,
1392
- "error_message": "Decision agents do not take attachments yet: send the case in the agent's fields."}
1393
1390
  if not str(model or "").strip() and not str(prompt_output_contract.get("llm_model") or "").strip():
1394
1391
  return {"error": True,
1395
1392
  "error_message": f"Agent '{prompt_name}' has no decision model: choose one in its settings."}
@@ -1409,6 +1406,17 @@ class QueryService:
1409
1406
  )
1410
1407
  question = json.dumps({'prompt': prompt_name, 'data': safe_client_data}, default=str)
1411
1408
 
1409
+ # Attachments follow the agent's attachment mode: extracted text joins the
1410
+ # case, images (and PDF pages, in native mode) travel as images.
1411
+ prepared = self.context_builder.prepare_decision_attachments(
1412
+ company,
1413
+ files or [],
1414
+ mode=prompt_output_contract.get("attachment_mode"),
1415
+ max_images=self._decision_max_images(company_short_name),
1416
+ )
1417
+ if prepared.text:
1418
+ state = f"{state}\n\n{prepared.text}" if state else prepared.text
1419
+
1412
1420
  execution_metadata: dict = {}
1413
1421
  request_source = str(safe_client_data.get("source") or "").strip().lower() or None
1414
1422
  if request_source:
@@ -1435,8 +1443,21 @@ class QueryService:
1435
1443
  execution_metadata=execution_metadata,
1436
1444
  telemetry_request=telemetry_request,
1437
1445
  execution_trace=execution_trace,
1446
+ images=prepared.images,
1447
+ attachments=prepared.manifest,
1438
1448
  )
1439
1449
 
1450
+ def _decision_max_images(self, company_short_name: str) -> int:
1451
+ from iatoolkit.services.decision_attachments import DEFAULT_MAX_IMAGES
1452
+
1453
+ llm_config = self.configuration_service.get_configuration(company_short_name, "llm") or {}
1454
+ decisions = llm_config.get("decisions") if isinstance(llm_config, dict) else None
1455
+ raw = decisions.get("max_images") if isinstance(decisions, dict) else None
1456
+ try:
1457
+ return int(raw) if raw is not None else DEFAULT_MAX_IMAGES
1458
+ except (TypeError, ValueError):
1459
+ return DEFAULT_MAX_IMAGES
1460
+
1440
1461
  def llm_query(self,
1441
1462
  company_short_name: str,
1442
1463
  user_identifier: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 2.77.1
3
+ Version: 2.77.3
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -130,6 +130,7 @@ src/iatoolkit/services/branding_service.py
130
130
  src/iatoolkit/services/company_context_service.py
131
131
  src/iatoolkit/services/configuration_service.py
132
132
  src/iatoolkit/services/context_builder_service.py
133
+ src/iatoolkit/services/decision_attachments.py
133
134
  src/iatoolkit/services/decision_service.py
134
135
  src/iatoolkit/services/dispatcher_service.py
135
136
  src/iatoolkit/services/embedding_service.py
File without changes
File without changes
File without changes
File without changes