sunholo 0.140.2__tar.gz → 0.140.5__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 (190) hide show
  1. {sunholo-0.140.2/src/sunholo.egg-info → sunholo-0.140.5}/PKG-INFO +1 -1
  2. {sunholo-0.140.2 → sunholo-0.140.5}/pyproject.toml +1 -1
  3. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/__init__.py +1 -1
  4. sunholo-0.140.5/src/sunholo/agents/chat_history.py +523 -0
  5. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/flask/__init__.py +0 -1
  6. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/flask/vac_routes.py +22 -24
  7. {sunholo-0.140.2 → sunholo-0.140.5/src/sunholo.egg-info}/PKG-INFO +1 -1
  8. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo.egg-info/SOURCES.txt +0 -1
  9. sunholo-0.140.2/src/sunholo/agents/chat_history.py +0 -245
  10. sunholo-0.140.2/src/sunholo/agents/flask/qna_routes.py +0 -604
  11. {sunholo-0.140.2 → sunholo-0.140.5}/LICENSE.txt +0 -0
  12. {sunholo-0.140.2 → sunholo-0.140.5}/MANIFEST.in +0 -0
  13. {sunholo-0.140.2 → sunholo-0.140.5}/README.md +0 -0
  14. {sunholo-0.140.2 → sunholo-0.140.5}/setup.cfg +0 -0
  15. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/__init__.py +0 -0
  16. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/dispatch_to_qa.py +0 -0
  17. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/fastapi/__init__.py +0 -0
  18. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/fastapi/base.py +0 -0
  19. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/fastapi/qna_routes.py +0 -0
  20. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/flask/base.py +0 -0
  21. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/langserve.py +0 -0
  22. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/pubsub.py +0 -0
  23. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/route.py +0 -0
  24. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/special_commands.py +0 -0
  25. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/agents/swagger.py +0 -0
  26. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/archive/__init__.py +0 -0
  27. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/archive/archive.py +0 -0
  28. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/auth/__init__.py +0 -0
  29. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/auth/gcloud.py +0 -0
  30. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/auth/refresh.py +0 -0
  31. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/auth/run.py +0 -0
  32. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/azure/__init__.py +0 -0
  33. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/azure/auth.py +0 -0
  34. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/azure/blobs.py +0 -0
  35. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/azure/event_grid.py +0 -0
  36. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/bots/__init__.py +0 -0
  37. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/bots/discord.py +0 -0
  38. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/bots/github_webhook.py +0 -0
  39. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/bots/webapp.py +0 -0
  40. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/__init__.py +0 -0
  41. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/azure.py +0 -0
  42. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/doc_handling.py +0 -0
  43. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/encode_metadata.py +0 -0
  44. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/images.py +0 -0
  45. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/loaders.py +0 -0
  46. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/message_data.py +0 -0
  47. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/pdfs.py +0 -0
  48. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/process_chunker_data.py +0 -0
  49. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/publish.py +0 -0
  50. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/pubsub.py +0 -0
  51. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/chunker/splitter.py +0 -0
  52. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/__init__.py +0 -0
  53. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/chat_vac.py +0 -0
  54. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/cli.py +0 -0
  55. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/cli_init.py +0 -0
  56. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/configs.py +0 -0
  57. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/deploy.py +0 -0
  58. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/embedder.py +0 -0
  59. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/merge_texts.py +0 -0
  60. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/run_proxy.py +0 -0
  61. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/sun_rich.py +0 -0
  62. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/swagger.py +0 -0
  63. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/cli/vertex.py +0 -0
  64. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/components/__init__.py +0 -0
  65. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/components/llm.py +0 -0
  66. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/components/retriever.py +0 -0
  67. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/components/vectorstore.py +0 -0
  68. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/custom_logging.py +0 -0
  69. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/__init__.py +0 -0
  70. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/alloydb.py +0 -0
  71. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/alloydb_client.py +0 -0
  72. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/database.py +0 -0
  73. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/lancedb.py +0 -0
  74. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/sql/sb/create_function.sql +0 -0
  75. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/sql/sb/create_function_time.sql +0 -0
  76. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/sql/sb/create_table.sql +0 -0
  77. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/sql/sb/delete_source_row.sql +0 -0
  78. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/sql/sb/return_sources.sql +0 -0
  79. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/sql/sb/setup.sql +0 -0
  80. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/static_dbs.py +0 -0
  81. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/database/uuid.py +0 -0
  82. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/discovery_engine/__init__.py +0 -0
  83. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/discovery_engine/chunker_handler.py +0 -0
  84. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/discovery_engine/cli.py +0 -0
  85. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/discovery_engine/create_new.py +0 -0
  86. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/discovery_engine/discovery_engine_client.py +0 -0
  87. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/discovery_engine/get_ai_search_chunks.py +0 -0
  88. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/embedder/__init__.py +0 -0
  89. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/embedder/embed_chunk.py +0 -0
  90. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/embedder/embed_metadata.py +0 -0
  91. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/excel/__init__.py +0 -0
  92. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/excel/plugin.py +0 -0
  93. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/__init__.py +0 -0
  94. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/add_file.py +0 -0
  95. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/download_folder.py +0 -0
  96. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/download_gcs_text.py +0 -0
  97. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/download_url.py +0 -0
  98. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/extract_and_sign.py +0 -0
  99. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/gcs/metadata.py +0 -0
  100. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/__init__.py +0 -0
  101. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/file_handling.py +0 -0
  102. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/genaiv2.py +0 -0
  103. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/images.py +0 -0
  104. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/init.py +0 -0
  105. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/process_funcs_cls.py +0 -0
  106. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/genai/safety.py +0 -0
  107. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/invoke/__init__.py +0 -0
  108. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/invoke/async_class.py +0 -0
  109. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/invoke/direct_vac_func.py +0 -0
  110. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/invoke/invoke_vac_utils.py +0 -0
  111. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/langchain_types.py +0 -0
  112. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/langfuse/__init__.py +0 -0
  113. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/langfuse/callback.py +0 -0
  114. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/langfuse/evals.py +0 -0
  115. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/langfuse/prompts.py +0 -0
  116. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/llamaindex/__init__.py +0 -0
  117. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/llamaindex/get_files.py +0 -0
  118. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/llamaindex/import_files.py +0 -0
  119. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/llamaindex/llamaindex_class.py +0 -0
  120. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/llamaindex/user_history.py +0 -0
  121. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/lookup/__init__.py +0 -0
  122. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/lookup/model_lookup.yaml +0 -0
  123. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/mcp/__init__.py +0 -0
  124. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/mcp/cli.py +0 -0
  125. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/ollama/__init__.py +0 -0
  126. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/ollama/ollama_images.py +0 -0
  127. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/pubsub/__init__.py +0 -0
  128. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/pubsub/process_pubsub.py +0 -0
  129. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/pubsub/pubsub_manager.py +0 -0
  130. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/qna/__init__.py +0 -0
  131. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/qna/parsers.py +0 -0
  132. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/qna/retry.py +0 -0
  133. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/senses/__init__.py +0 -0
  134. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/senses/stream_voice.py +0 -0
  135. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/streaming/__init__.py +0 -0
  136. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/streaming/content_buffer.py +0 -0
  137. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/streaming/langserve.py +0 -0
  138. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/streaming/stream_lookup.py +0 -0
  139. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/streaming/streaming.py +0 -0
  140. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/summarise/__init__.py +0 -0
  141. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/summarise/summarise.py +0 -0
  142. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/__init__.py +0 -0
  143. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/agent_service.py +0 -0
  144. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/app.py +0 -0
  145. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/my_log.py +0 -0
  146. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/tools/__init__.py +0 -0
  147. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/tools/your_agent.py +0 -0
  148. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/agent/vac_service.py +0 -0
  149. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/project/__init__.py +0 -0
  150. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/project/app.py +0 -0
  151. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/project/my_log.py +0 -0
  152. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/project/vac_service.py +0 -0
  153. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/system_services/__init__.py +0 -0
  154. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/system_services/app.py +0 -0
  155. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/templates/system_services/my_log.py +0 -0
  156. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/terraform/__init__.py +0 -0
  157. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/terraform/tfvars_editor.py +0 -0
  158. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/tools/__init__.py +0 -0
  159. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/tools/web_browser.py +0 -0
  160. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/__init__.py +0 -0
  161. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/api_key.py +0 -0
  162. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/big_context.py +0 -0
  163. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/config.py +0 -0
  164. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/config_class.py +0 -0
  165. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/config_schema.py +0 -0
  166. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/gcp.py +0 -0
  167. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/gcp_project.py +0 -0
  168. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/mime.py +0 -0
  169. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/parsers.py +0 -0
  170. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/timedelta.py +0 -0
  171. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/user_ids.py +0 -0
  172. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/utils/version.py +0 -0
  173. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/__init__.py +0 -0
  174. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/extensions_call.py +0 -0
  175. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/extensions_class.py +0 -0
  176. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/genai_functions.py +0 -0
  177. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/init.py +0 -0
  178. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/memory_tools.py +0 -0
  179. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/safety.py +0 -0
  180. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo/vertex/type_dict_to_json.py +0 -0
  181. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo.egg-info/dependency_links.txt +0 -0
  182. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo.egg-info/entry_points.txt +0 -0
  183. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo.egg-info/requires.txt +0 -0
  184. {sunholo-0.140.2 → sunholo-0.140.5}/src/sunholo.egg-info/top_level.txt +0 -0
  185. {sunholo-0.140.2 → sunholo-0.140.5}/tests/test_async.py +0 -0
  186. {sunholo-0.140.2 → sunholo-0.140.5}/tests/test_async_genai2.py +0 -0
  187. {sunholo-0.140.2 → sunholo-0.140.5}/tests/test_chat_history.py +0 -0
  188. {sunholo-0.140.2 → sunholo-0.140.5}/tests/test_config.py +0 -0
  189. {sunholo-0.140.2 → sunholo-0.140.5}/tests/test_genai2.py +0 -0
  190. {sunholo-0.140.2 → sunholo-0.140.5}/tests/test_unstructured.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.140.2
3
+ Version: 0.140.5
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sunholo"
7
- version = "0.140.2"
7
+ version = "0.140.5"
8
8
  description = "AI DevOps - a package to help deploy GenAI to the Cloud."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,6 +2,6 @@ from .chat_history import extract_chat_history
2
2
  from .dispatch_to_qa import send_to_qa, send_to_qa_async
3
3
  from .pubsub import process_pubsub
4
4
  from .special_commands import handle_special_commands, app_to_store, handle_files
5
- from .flask import register_qna_routes, create_app, VACRoutes
5
+ from .flask import create_app, VACRoutes
6
6
  from .fastapi import register_qna_fastapi_routes, create_fastapi_app
7
7
  from .swagger import config_to_swagger
@@ -0,0 +1,523 @@
1
+ import json
2
+ from ..custom_logging import log
3
+ import time
4
+ import hashlib
5
+ from functools import lru_cache
6
+ from typing import List, Tuple, Optional
7
+
8
+
9
+ class ChatHistoryCache:
10
+ """
11
+ Incremental cache for chat history processing.
12
+
13
+ Caches processed message pairs and only processes new messages
14
+ when the chat history is extended.
15
+ """
16
+
17
+ def __init__(self, max_cache_size: int = 1000):
18
+ self.cache = {}
19
+ self.max_cache_size = max_cache_size
20
+
21
+ def _get_cache_key(self, chat_history: List[dict]) -> str:
22
+ """Generate a cache key based on the chat history content."""
23
+ # Use the hash of the serialized chat history for the key
24
+ # Only hash the first few and last few messages to balance performance vs accuracy
25
+ if len(chat_history) <= 10:
26
+ content = str(chat_history)
27
+ else:
28
+ # Hash first 5 and last 5 messages + length
29
+ content = str(chat_history[:5] + chat_history[-5:] + [len(chat_history)])
30
+
31
+ return hashlib.md5(content.encode()).hexdigest()
32
+
33
+ def _find_cached_prefix(self, current_history: List[dict]) -> Tuple[Optional[List[Tuple]], int]:
34
+ """
35
+ Find the longest cached prefix of the current chat history.
36
+
37
+ Returns:
38
+ Tuple of (cached_pairs, cache_length) or (None, 0) if no cache found
39
+ """
40
+ current_length = len(current_history)
41
+
42
+ # Check for cached versions of prefixes, starting from longest
43
+ for cache_length in range(current_length - 1, 0, -1):
44
+ prefix = current_history[:cache_length]
45
+ cache_key = self._get_cache_key(prefix)
46
+
47
+ if cache_key in self.cache:
48
+ cached_data = self.cache[cache_key]
49
+ cached_pairs = cached_data['pairs']
50
+
51
+ # Verify the cache is still valid by checking a few messages
52
+ if self._verify_cache_validity(prefix, cached_data['original_history']):
53
+ return cached_pairs, cache_length
54
+ else:
55
+ # Cache is stale, remove it
56
+ del self.cache[cache_key]
57
+
58
+ return None, 0
59
+
60
+ def _verify_cache_validity(self, current_prefix: List[dict], cached_prefix: List[dict]) -> bool:
61
+ """Quick verification that cached data is still valid."""
62
+ if len(current_prefix) != len(cached_prefix):
63
+ return False
64
+
65
+ # Check first and last few messages for equality
66
+ check_indices = [0, -1] if len(current_prefix) >= 2 else [0]
67
+
68
+ for i in check_indices:
69
+ if current_prefix[i] != cached_prefix[i]:
70
+ return False
71
+
72
+ return True
73
+
74
+ def extract_chat_history_incremental(self, chat_history: List[dict]) -> List[Tuple]:
75
+ """
76
+ Extract chat history with incremental caching.
77
+
78
+ Args:
79
+ chat_history: List of chat message dictionaries
80
+
81
+ Returns:
82
+ List of (human_message, ai_message) tuples
83
+ """
84
+ if not chat_history:
85
+ return []
86
+
87
+ # Try to find cached prefix
88
+ cached_pairs, cache_length = self._find_cached_prefix(chat_history)
89
+
90
+ if cached_pairs is not None:
91
+ log.debug(f"Found cached pairs for {cache_length} messages, processing {len(chat_history) - cache_length} new messages")
92
+
93
+ # Process only the new messages
94
+ new_messages = chat_history[cache_length:]
95
+ new_pairs = self._process_new_messages(new_messages, cached_pairs)
96
+
97
+ # Combine cached and new pairs
98
+ all_pairs = cached_pairs + new_pairs
99
+ else:
100
+ log.debug(f"No cache found, processing all {len(chat_history)} messages")
101
+ # Process all messages from scratch
102
+ all_pairs = self._extract_chat_history_full(chat_history)
103
+
104
+ # Cache the result
105
+ self._update_cache(chat_history, all_pairs)
106
+
107
+ return all_pairs
108
+
109
+ def _process_new_messages(self, new_messages: List[dict], cached_pairs: List[Tuple]) -> List[Tuple]:
110
+ """
111
+ Process only the new messages, considering the state from cached pairs.
112
+
113
+ Args:
114
+ new_messages: New messages to process
115
+ cached_pairs: Previously processed message pairs
116
+
117
+ Returns:
118
+ List of new message pairs
119
+ """
120
+ if not new_messages:
121
+ return []
122
+
123
+ new_pairs = []
124
+
125
+ # Determine if we're waiting for a bot response based on cached pairs
126
+ waiting_for_bot = True
127
+ if cached_pairs:
128
+ last_pair = cached_pairs[-1]
129
+ # If last pair has both human and AI message, we're ready for a new human message
130
+ waiting_for_bot = not (last_pair[0] and last_pair[1])
131
+
132
+ # If we ended with an unpaired human message, get it
133
+ last_human_message = ""
134
+ if cached_pairs and waiting_for_bot:
135
+ last_human_message = cached_pairs[-1][0]
136
+
137
+ # Process new messages
138
+ for message in new_messages:
139
+ try:
140
+ is_human_msg = is_human(message)
141
+ content = create_message_element(message)
142
+
143
+ if is_human_msg:
144
+ last_human_message = content
145
+ waiting_for_bot = True
146
+ else: # Bot message
147
+ if waiting_for_bot and last_human_message:
148
+ new_pairs.append((last_human_message, content))
149
+ last_human_message = ""
150
+ waiting_for_bot = False
151
+ # If not waiting for bot or no human message, this is an orphaned bot message
152
+
153
+ except (KeyError, TypeError) as e:
154
+ log.warning(f"Error processing new message: {e}")
155
+ continue
156
+
157
+ return new_pairs
158
+
159
+ def _extract_chat_history_full(self, chat_history: List[dict]) -> List[Tuple]:
160
+ """Full extraction when no cache is available."""
161
+ # Use the optimized version from before
162
+ paired_messages = []
163
+
164
+ # Handle initial bot message
165
+ start_idx = 0
166
+ if chat_history and is_bot(chat_history[0]):
167
+ try:
168
+ first_message = chat_history[0]
169
+ blank_element = ""
170
+ bot_element = create_message_element(first_message)
171
+ paired_messages.append((blank_element, bot_element))
172
+ start_idx = 1
173
+ except (KeyError, TypeError):
174
+ pass
175
+
176
+ # Process remaining messages
177
+ last_human_message = ""
178
+ for i in range(start_idx, len(chat_history)):
179
+ message = chat_history[i]
180
+
181
+ try:
182
+ is_human_msg = is_human(message)
183
+ content = create_message_element(message)
184
+
185
+ if is_human_msg:
186
+ last_human_message = content
187
+ else: # Bot message
188
+ if last_human_message:
189
+ paired_messages.append((last_human_message, content))
190
+ last_human_message = ""
191
+
192
+ except (KeyError, TypeError) as e:
193
+ log.warning(f"Error processing message {i}: {e}")
194
+ continue
195
+
196
+ return paired_messages
197
+
198
+ def _update_cache(self, chat_history: List[dict], pairs: List[Tuple]):
199
+ """Update cache with new result."""
200
+ # Only cache if the history is of reasonable size
201
+ if len(chat_history) < 2:
202
+ return
203
+
204
+ cache_key = self._get_cache_key(chat_history)
205
+
206
+ # Implement simple LRU by removing oldest entries
207
+ if len(self.cache) >= self.max_cache_size:
208
+ # Remove 20% of oldest entries
209
+ remove_count = self.max_cache_size // 5
210
+ oldest_keys = list(self.cache.keys())[:remove_count]
211
+ for key in oldest_keys:
212
+ del self.cache[key]
213
+
214
+ self.cache[cache_key] = {
215
+ 'pairs': pairs,
216
+ 'original_history': chat_history.copy(), # Store copy for validation
217
+ 'timestamp': time.time()
218
+ }
219
+
220
+ log.debug(f"Cached {len(pairs)} pairs for history of length {len(chat_history)}")
221
+
222
+ def clear_cache(self):
223
+ """Clear the entire cache."""
224
+ self.cache.clear()
225
+ log.info("Chat history cache cleared")
226
+
227
+
228
+ # Global cache instance
229
+ _chat_history_cache = ChatHistoryCache()
230
+
231
+
232
+ def extract_chat_history_with_cache(chat_history: List[dict] = None) -> List[Tuple]:
233
+ """
234
+ Main function to replace the original extract_chat_history.
235
+
236
+ Uses incremental caching for better performance with growing chat histories.
237
+ """
238
+ if not chat_history:
239
+ log.debug("No chat history found")
240
+ return []
241
+
242
+ return _chat_history_cache.extract_chat_history_incremental(chat_history)
243
+
244
+
245
+ # Async version that wraps the cached version
246
+ async def extract_chat_history_async_cached(chat_history: List[dict] = None) -> List[Tuple]:
247
+ """
248
+ Async version that uses the cache and runs in a thread pool if needed.
249
+ """
250
+ import asyncio
251
+
252
+ if not chat_history:
253
+ return []
254
+
255
+ # For very large histories, run in thread pool to avoid blocking
256
+ if len(chat_history) > 1000:
257
+ loop = asyncio.get_event_loop()
258
+ return await loop.run_in_executor(
259
+ None,
260
+ extract_chat_history_with_cache,
261
+ chat_history
262
+ )
263
+ else:
264
+ # For smaller histories, just run directly
265
+ return extract_chat_history_with_cache(chat_history)
266
+
267
+
268
+ # Utility function to warm up the cache
269
+ def warm_up_cache(chat_histories: List[List[dict]]):
270
+ """
271
+ Pre-populate cache with common chat histories.
272
+
273
+ Args:
274
+ chat_histories: List of chat history lists to cache
275
+ """
276
+ for history in chat_histories:
277
+ extract_chat_history_with_cache(history)
278
+
279
+ log.info(f"Warmed up cache with {len(chat_histories)} chat histories")
280
+
281
+
282
+ async def extract_chat_history_async(chat_history=None):
283
+ """
284
+ Extracts paired chat history between human and AI messages.
285
+
286
+ For this lightweight processing, we use a simpler approach that minimizes overhead.
287
+
288
+ Args:
289
+ chat_history (list): List of chat messages.
290
+
291
+ Returns:
292
+ list: List of tuples with paired human and AI messages.
293
+ """
294
+ if not chat_history:
295
+ log.info("No chat history found")
296
+ return []
297
+
298
+ log.info(f"Extracting chat history: {chat_history}")
299
+ paired_messages = []
300
+
301
+ # Handle special case of initial bot message
302
+ if chat_history and is_bot(chat_history[0]):
303
+ first_message = chat_history[0]
304
+ log.info(f"Extracting first_message: {first_message}")
305
+ blank_human_message = {"name": "Human", "content": "", "embeds": []}
306
+
307
+ # Since create_message_element is so lightweight, we don't need async here
308
+ blank_element = create_message_element(blank_human_message)
309
+ bot_element = create_message_element(first_message)
310
+
311
+ paired_messages.append((blank_element, bot_element))
312
+ chat_history = chat_history[1:]
313
+
314
+ # Pre-process all messages in one batch (more efficient than one-by-one)
315
+ message_types = []
316
+ message_contents = []
317
+
318
+ for message in chat_history:
319
+ is_human_msg = is_human(message)
320
+ is_bot_msg = is_bot(message)
321
+
322
+ # Extract content for all messages at once
323
+ content = create_message_element(message)
324
+
325
+ message_types.append((is_human_msg, is_bot_msg))
326
+ message_contents.append(content)
327
+
328
+ # Pair messages efficiently
329
+ last_human_message = ""
330
+ for i, ((is_human_msg, is_bot_msg), content) in enumerate(zip(message_types, message_contents)):
331
+ if is_human_msg:
332
+ last_human_message = content
333
+ log.info(f"Extracted human message: {last_human_message}")
334
+ elif is_bot_msg:
335
+ ai_message = content
336
+ log.info(f"Extracted AI message: {ai_message}")
337
+ paired_messages.append((last_human_message, ai_message))
338
+ last_human_message = ""
339
+
340
+ log.info(f"Paired messages: {paired_messages}")
341
+ return paired_messages
342
+
343
+
344
+ def extract_chat_history(chat_history=None):
345
+ """
346
+ Extracts paired chat history between human and AI messages.
347
+
348
+ This function takes a chat history and returns a list of pairs of messages,
349
+ where each pair consists of a human message followed by the corresponding AI response.
350
+
351
+ Args:
352
+ chat_history (list): List of chat messages.
353
+
354
+ Returns:
355
+ list: List of tuples with paired human and AI messages.
356
+
357
+ Example:
358
+ ```python
359
+ chat_history = [
360
+ {"name": "Human", "text": "Hello, AI!"},
361
+ {"name": "AI", "text": "Hello, Human! How can I help you today?"}
362
+ ]
363
+ paired_messages = extract_chat_history(chat_history)
364
+ print(paired_messages)
365
+ # Output: [("Hello, AI!", "Hello, Human! How can I help you today?")]
366
+ ```
367
+ """
368
+ if not chat_history:
369
+ log.info("No chat history found")
370
+ return []
371
+
372
+ log.info(f"Extracting chat history: {chat_history}")
373
+ paired_messages = []
374
+
375
+ first_message = chat_history[0]
376
+ log.info(f"Extracting first_message: {first_message}")
377
+ if is_bot(first_message):
378
+ blank_human_message = {"name": "Human", "content": "", "embeds": []}
379
+ paired_messages.append((create_message_element(blank_human_message),
380
+ create_message_element(first_message)))
381
+ chat_history = chat_history[1:]
382
+
383
+ last_human_message = ""
384
+ for message in chat_history:
385
+ log.info(f"Extracing message: {message}")
386
+ if is_human(message):
387
+ last_human_message = create_message_element(message)
388
+ log.info(f"Extracted human message: {last_human_message}")
389
+ elif is_bot(message):
390
+ ai_message = create_message_element(message)
391
+ log.info(f"Extracted AI message: {ai_message}")
392
+ paired_messages.append((last_human_message, ai_message))
393
+ last_human_message = ""
394
+
395
+ log.info(f"Paired messages: {paired_messages}")
396
+
397
+ return paired_messages
398
+
399
+ def embeds_to_json(message: dict):
400
+ """
401
+ Converts the 'embeds' field in a message to a JSON string.
402
+
403
+ Args:
404
+ message (dict): The message containing the 'embeds' field.
405
+
406
+ Returns:
407
+ str: JSON string representation of the 'embeds' field or an empty string if no embeds are found.
408
+
409
+ Example:
410
+ ```python
411
+ message = {"embeds": [{"type": "image", "url": "https://example.com/image.png"}]}
412
+ json_string = embeds_to_json(message)
413
+ print(json_string)
414
+ # Output: '[{"type": "image", "url": "https://example.com/image.png"}]'
415
+ ```
416
+ """
417
+ if 'embeds' in message and len(message['embeds']) > 0:
418
+ return json.dumps(message.get("embeds"))
419
+ else:
420
+ return ""
421
+
422
+ def create_message_element(message: dict):
423
+ """
424
+ Extracts the main content of a message.
425
+
426
+ Args:
427
+ message (dict): The message to extract content from.
428
+
429
+ Returns:
430
+ str: The text or content of the message.
431
+
432
+ Raises:
433
+ KeyError: If neither 'content' nor 'text' fields are found.
434
+
435
+ Example:
436
+ ```python
437
+ message = {"text": "Hello, AI!"}
438
+ content = create_message_element(message)
439
+ print(content)
440
+ # Output: 'Hello, AI!'
441
+ ```
442
+ """
443
+ if 'text' in message: # This is a Slack or Google Chat message
444
+ log.info(f"Found text element - {message['text']}")
445
+ return message['text']
446
+ elif 'content' in message: # Discord or OpenAI history message
447
+ log.info(f"Found content element - {message['content']}")
448
+ return message['content']
449
+ else:
450
+ raise KeyError(f"Could not extract 'content' or 'text' element from message: {message}, {type(message)}")
451
+
452
+ def is_human(message: dict):
453
+ """
454
+ Checks if a message was sent by a human.
455
+
456
+ Args:
457
+ message (dict): The message to check.
458
+
459
+ Returns:
460
+ bool: True if the message was sent by a human, otherwise False.
461
+
462
+ Example:
463
+ ```python
464
+ message = {"name": "Human"}
465
+ print(is_human(message))
466
+ # Output: True
467
+ ```
468
+ """
469
+ if 'name' in message:
470
+ return message["name"] == "Human"
471
+ elif 'sender' in message: # Google Chat
472
+ return message['sender']['type'] == 'HUMAN'
473
+ elif 'role' in message:
474
+ return message['role'] == 'user'
475
+ else:
476
+ # Slack: Check for the 'user' field and absence of 'bot_id' field
477
+ return 'user' in message and 'bot_id' not in message
478
+
479
+ def is_bot(message: dict):
480
+ """
481
+ Checks if a message was sent by a bot.
482
+
483
+ Args:
484
+ message (dict): The message to check.
485
+
486
+ Returns:
487
+ bool: True if the message was sent by a bot, otherwise False.
488
+
489
+ Example:
490
+ ```python
491
+ message = {"name": "AI"}
492
+ print(is_bot(message))
493
+ # Output: True
494
+ ```
495
+ """
496
+ return not is_human(message)
497
+
498
+ def is_ai(message: dict):
499
+ """
500
+ Checks if a message was specifically sent by an AI.
501
+
502
+ Args:
503
+ message (dict): The message to check.
504
+
505
+ Returns:
506
+ bool: True if the message was sent by an AI, otherwise False.
507
+
508
+ Example:
509
+ ```python
510
+ message = {"name": "AI"}
511
+ print(is_ai(message))
512
+ # Output: True
513
+ ```
514
+ """
515
+ if 'name' in message:
516
+ return message["name"] == "AI"
517
+ elif 'sender' in message: # Google Chat
518
+ return message['sender']['type'] == 'BOT'
519
+ elif 'role' in message:
520
+ return message['role'] == 'assistant'
521
+ else:
522
+ return 'bot_id' in message # Slack
523
+
@@ -1,3 +1,2 @@
1
- from .qna_routes import register_qna_routes
2
1
  from .base import create_app
3
2
  from .vac_routes import VACRoutes
@@ -7,8 +7,8 @@ from functools import partial
7
7
  import inspect
8
8
  import asyncio
9
9
 
10
- from ...agents import extract_chat_history, handle_special_commands
11
- from ..chat_history import extract_chat_history_async
10
+ from ...agents import handle_special_commands
11
+ from ..chat_history import extract_chat_history_with_cache, extract_chat_history_async_cached
12
12
  from ...qna.parsers import parse_output
13
13
  from ...streaming import start_streaming_chat, start_streaming_chat_async
14
14
  from ...archive import archive_qa
@@ -58,12 +58,18 @@ if __name__ == "__main__":
58
58
  ```
59
59
 
60
60
  """
61
- def __init__(self, app, stream_interpreter: callable, vac_interpreter:callable=None, additional_routes:dict=None, async_stream:bool=False):
61
+ def __init__(self, app,
62
+ stream_interpreter: callable,
63
+ vac_interpreter:callable=None,
64
+ additional_routes:dict=None,
65
+ async_stream:bool=False,
66
+ add_langfuse_eval:bool=True):
62
67
  self.app = app
63
68
  self.stream_interpreter = stream_interpreter
64
69
  self.vac_interpreter = vac_interpreter or partial(self.vac_interpreter_default)
65
70
  self.additional_routes = additional_routes if additional_routes is not None else []
66
71
  self.async_stream = async_stream
72
+ self.add_langfuse_eval = add_langfuse_eval
67
73
  self.register_routes()
68
74
 
69
75
 
@@ -96,13 +102,9 @@ if __name__ == "__main__":
96
102
  # Basic routes
97
103
  self.app.route("/", methods=['GET'])(self.home)
98
104
  self.app.route("/health", methods=['GET'])(self.health)
99
-
100
- # Streaming VAC
101
- self.app.route('/vac/streaming/<vector_name>',
102
- methods=['POST'],
103
- provide_automatic_options=False)(self.handle_stream_vac)
104
105
 
105
106
  if self.async_stream: # Use async treatment
107
+ log.info("async_stream enabled")
106
108
  self.app.route('/vac/streaming/<vector_name>',
107
109
  methods=['POST'],
108
110
  provide_automatic_options=False)(self.handle_stream_vac_async)
@@ -351,10 +353,10 @@ if __name__ == "__main__":
351
353
 
352
354
  # Use the async version of prep_vac
353
355
  prep = await self.prep_vac_async(request, vector_name)
354
- log.info(f"Processing prep: {prep}")
356
+ log.info(f"Processing async prep: {prep}")
355
357
  all_input = prep["all_input"]
356
358
 
357
- log.info(f'Streaming data with: {all_input}')
359
+ log.info(f'Streaming async data with: {all_input}')
358
360
 
359
361
  async def generate_response_content():
360
362
  try:
@@ -378,12 +380,12 @@ if __name__ == "__main__":
378
380
  yield chunk
379
381
 
380
382
  except Exception as e:
381
- yield f"Streaming Error: {str(e)} {traceback.format_exc()}"
383
+ yield f"Streaming async Error: {str(e)} {traceback.format_exc()}"
382
384
 
383
385
  response = Response(generate_response_content(), content_type='text/plain; charset=utf-8')
384
386
  response.headers['Transfer-Encoding'] = 'chunked'
385
387
 
386
- log.debug(f"streaming response: {response}")
388
+ log.debug(f"streaming async response: {response}")
387
389
 
388
390
  return response
389
391
 
@@ -554,7 +556,8 @@ if __name__ == "__main__":
554
556
  else:
555
557
  log.info(f"User message: {user_message}")
556
558
 
557
- paired_messages = extract_chat_history(chat_history)
559
+ paired_messages = extract_chat_history_with_cache(chat_history)
560
+
558
561
  command_response = handle_special_commands(user_message, vector_name, paired_messages)
559
562
 
560
563
  if command_response is not None:
@@ -698,10 +701,10 @@ if __name__ == "__main__":
698
701
 
699
702
  trace = None
700
703
  span = None
701
-
702
- trace_id = data.get('trace_id')
703
- trace = self.create_langfuse_trace(request, vector_name, trace_id)
704
- log.info(f"Using existing langfuse trace: {trace_id}")
704
+ if self.add_langfuse_eval:
705
+ trace_id = data.get('trace_id')
706
+ trace = self.create_langfuse_trace(request, vector_name, trace_id)
707
+ log.info(f"Using existing langfuse trace: {trace_id}")
705
708
 
706
709
  #config, _ = load_config("config/llm_config.yaml")
707
710
  try:
@@ -725,7 +728,7 @@ if __name__ == "__main__":
725
728
  vector_name = data.pop('vector_name', vector_name)
726
729
  data.pop('trace_id', None) # to ensure not in kwargs
727
730
 
728
- paired_messages = extract_chat_history(chat_history)
731
+ paired_messages = extract_chat_history_with_cache(chat_history)
729
732
 
730
733
  all_input = {'user_input': user_input,
731
734
  'vector_name': vector_name,
@@ -741,15 +744,10 @@ if __name__ == "__main__":
741
744
  metadata=vac_config.configs_by_kind,
742
745
  input = all_input
743
746
  )
744
- command_response = handle_special_commands(user_input, vector_name, paired_messages)
745
- if command_response is not None:
746
- if trace:
747
- trace.update(output=jsonify(command_response))
748
747
 
749
748
  return {
750
749
  "trace": trace,
751
750
  "span": span,
752
- "command_response": command_response,
753
751
  "all_input": all_input,
754
752
  "vac_config": vac_config
755
753
  }
@@ -793,7 +791,7 @@ if __name__ == "__main__":
793
791
  data.pop('trace_id', None) # to ensure not in kwargs
794
792
 
795
793
  # Task 3: Process chat history
796
- chat_history_task = asyncio.create_task(extract_chat_history_async(chat_history))
794
+ chat_history_task = asyncio.create_task(extract_chat_history_async_cached(chat_history))
797
795
  tasks.append(chat_history_task)
798
796
 
799
797
  # Await all tasks concurrently
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.140.2
3
+ Version: 0.140.5
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0