mito-ai 0.1.50__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mito_ai/__init__.py +114 -0
- mito_ai/_version.py +4 -0
- mito_ai/anthropic_client.py +334 -0
- mito_ai/app_deploy/__init__.py +6 -0
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/app_deploy/models.py +98 -0
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/__init__.py +3 -0
- mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
- mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
- mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
- mito_ai/completions/completion_handlers/completion_handler.py +42 -0
- mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
- mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
- mito_ai/completions/completion_handlers/utils.py +147 -0
- mito_ai/completions/handlers.py +415 -0
- mito_ai/completions/message_history.py +401 -0
- mito_ai/completions/models.py +404 -0
- mito_ai/completions/prompt_builders/__init__.py +3 -0
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
- mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
- mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
- mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
- mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
- mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
- mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
- mito_ai/completions/prompt_builders/utils.py +84 -0
- mito_ai/completions/providers.py +284 -0
- mito_ai/constants.py +63 -0
- mito_ai/db/__init__.py +3 -0
- mito_ai/db/crawlers/__init__.py +6 -0
- mito_ai/db/crawlers/base_crawler.py +61 -0
- mito_ai/db/crawlers/constants.py +43 -0
- mito_ai/db/crawlers/snowflake.py +71 -0
- mito_ai/db/handlers.py +168 -0
- mito_ai/db/models.py +31 -0
- mito_ai/db/urls.py +34 -0
- mito_ai/db/utils.py +185 -0
- mito_ai/docker/mssql/compose.yml +37 -0
- mito_ai/docker/mssql/init/setup.sql +21 -0
- mito_ai/docker/mysql/compose.yml +18 -0
- mito_ai/docker/mysql/init/setup.sql +13 -0
- mito_ai/docker/oracle/compose.yml +17 -0
- mito_ai/docker/oracle/init/setup.sql +20 -0
- mito_ai/docker/postgres/compose.yml +17 -0
- mito_ai/docker/postgres/init/setup.sql +13 -0
- mito_ai/enterprise/__init__.py +3 -0
- mito_ai/enterprise/utils.py +15 -0
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +232 -0
- mito_ai/log/handlers.py +38 -0
- mito_ai/log/urls.py +21 -0
- mito_ai/logger.py +37 -0
- mito_ai/openai_client.py +382 -0
- mito_ai/path_utils.py +70 -0
- mito_ai/rules/handlers.py +44 -0
- mito_ai/rules/urls.py +22 -0
- mito_ai/rules/utils.py +56 -0
- mito_ai/settings/handlers.py +41 -0
- mito_ai/settings/urls.py +20 -0
- mito_ai/settings/utils.py +42 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/__init__.py +3 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/conftest.py +53 -0
- mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
- mito_ai/tests/data/prompt_lg.py +69 -0
- mito_ai/tests/data/prompt_sm.py +6 -0
- mito_ai/tests/data/prompt_xl.py +13 -0
- mito_ai/tests/data/stock_data.sqlite3 +0 -0
- mito_ai/tests/db/conftest.py +39 -0
- mito_ai/tests/db/connections_test.py +102 -0
- mito_ai/tests/db/mssql_test.py +29 -0
- mito_ai/tests/db/mysql_test.py +29 -0
- mito_ai/tests/db/oracle_test.py +29 -0
- mito_ai/tests/db/postgres_test.py +29 -0
- mito_ai/tests/db/schema_test.py +93 -0
- mito_ai/tests/db/sqlite_test.py +31 -0
- mito_ai/tests/db/test_db_constants.py +61 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
- mito_ai/tests/message_history/test_message_history_utils.py +469 -0
- mito_ai/tests/open_ai_utils_test.py +152 -0
- mito_ai/tests/performance_test.py +329 -0
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +631 -0
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/providers/test_gemini_client.py +195 -0
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/rules/conftest.py +26 -0
- mito_ai/tests/rules/rules_test.py +117 -0
- mito_ai/tests/server_limits_test.py +406 -0
- mito_ai/tests/settings/conftest.py +26 -0
- mito_ai/tests/settings/settings_test.py +70 -0
- mito_ai/tests/settings/test_settings_constants.py +9 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +47 -0
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/__init__.py +3 -0
- mito_ai/tests/utils/test_anthropic_utils.py +162 -0
- mito_ai/tests/utils/test_gemini_utils.py +98 -0
- mito_ai/tests/version_check_test.py +169 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/__init__.py +3 -0
- mito_ai/utils/anthropic_utils.py +168 -0
- mito_ai/utils/create.py +94 -0
- mito_ai/utils/db.py +74 -0
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +133 -0
- mito_ai/utils/message_history_utils.py +87 -0
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +200 -0
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/schema.py +86 -0
- mito_ai/utils/server_limits.py +152 -0
- mito_ai/utils/telemetry_utils.py +480 -0
- mito_ai/utils/utils.py +89 -0
- mito_ai/utils/version_utils.py +94 -0
- mito_ai/utils/websocket_base.py +88 -0
- mito_ai/version_check.py +60 -0
- mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +533 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +7440 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +4859 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +1 -0
- mito_ai-0.1.50.dist-info/METADATA +221 -0
- mito_ai-0.1.50.dist-info/RECORD +205 -0
- mito_ai-0.1.50.dist-info/WHEEL +4 -0
- mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
- mito_ai-0.1.50.dist-info/licenses/LICENSE +3 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from threading import Lock
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
12
|
+
from mito_ai.completions.models import CompletionRequest, ChatThreadMetadata, MessageType, ThreadID
|
|
13
|
+
from mito_ai.completions.prompt_builders.chat_name_prompt import create_chat_name_prompt
|
|
14
|
+
from mito_ai.completions.providers import OpenAIProvider
|
|
15
|
+
from mito_ai.utils.schema import MITO_FOLDER
|
|
16
|
+
from mito_ai.utils.message_history_utils import trim_old_messages
|
|
17
|
+
|
|
18
|
+
CHAT_HISTORY_VERSION = 2 # Increment this if the schema changes
|
|
19
|
+
NEW_CHAT_NAME = "(New Chat)"
|
|
20
|
+
NUMBER_OF_THREADS_CUT_OFF = 50
|
|
21
|
+
|
|
22
|
+
async def generate_short_chat_name(user_message: str, assistant_message: str, model: str, llm_provider: OpenAIProvider) -> str:
|
|
23
|
+
prompt = create_chat_name_prompt(user_message, assistant_message)
|
|
24
|
+
|
|
25
|
+
completion = await llm_provider.request_completions(
|
|
26
|
+
messages=[{"role": "user", "content": prompt}],
|
|
27
|
+
# We set the model so we can use the correct model provider, but request_completions will decide to
|
|
28
|
+
# use the fast model from that provider to make the request.
|
|
29
|
+
model=model,
|
|
30
|
+
message_type=MessageType.CHAT_NAME_GENERATION,
|
|
31
|
+
thread_id=None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Do a little cleanup of the completion. Gemini seems to return the string
|
|
35
|
+
# wrapped in quotes and a newline character.
|
|
36
|
+
# TODO: Fix this problem upstream. I wonder if there is some extra encoding
|
|
37
|
+
# we are doing with the gemini responses.
|
|
38
|
+
if isinstance(completion, str):
|
|
39
|
+
# If completion has quotes around it, remove them
|
|
40
|
+
if completion.startswith('"') and completion.endswith('"'):
|
|
41
|
+
completion = completion[1:-1]
|
|
42
|
+
|
|
43
|
+
if completion.startswith("'") and completion.endswith("'"):
|
|
44
|
+
completion = completion[1:-1]
|
|
45
|
+
|
|
46
|
+
# If completion ends in a \n, remove it
|
|
47
|
+
completion = completion.strip()
|
|
48
|
+
completion = completion.replace("\n", "") # Remove newline character
|
|
49
|
+
completion = completion.replace("\\n", "") # Remove literal \ and n characters
|
|
50
|
+
|
|
51
|
+
if not completion or completion == "":
|
|
52
|
+
return "Untitled Chat"
|
|
53
|
+
|
|
54
|
+
return completion
|
|
55
|
+
|
|
56
|
+
class ChatThread:
|
|
57
|
+
"""
|
|
58
|
+
Holds metadata + two lists of messages: LLM and display messages.
|
|
59
|
+
"""
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
thread_id: ThreadID,
|
|
63
|
+
creation_ts: float,
|
|
64
|
+
last_interaction_ts: float,
|
|
65
|
+
name: str,
|
|
66
|
+
ai_optimized_history: List[ChatCompletionMessageParam] = [],
|
|
67
|
+
display_history: List[ChatCompletionMessageParam] = [],
|
|
68
|
+
):
|
|
69
|
+
self.thread_id = thread_id
|
|
70
|
+
self.creation_ts = creation_ts
|
|
71
|
+
self.last_interaction_ts = last_interaction_ts
|
|
72
|
+
self.name = name # short name for the thread
|
|
73
|
+
self.ai_optimized_history: List[ChatCompletionMessageParam] = ai_optimized_history or []
|
|
74
|
+
self.display_history: List[ChatCompletionMessageParam] = display_history or []
|
|
75
|
+
self.chat_history_version = CHAT_HISTORY_VERSION
|
|
76
|
+
|
|
77
|
+
class GlobalMessageHistory:
|
|
78
|
+
"""
|
|
79
|
+
Manages a global message history with thread-safe chat conversations.
|
|
80
|
+
|
|
81
|
+
This class ensures thread-safe operations for reading, writing, and
|
|
82
|
+
modifying message histories using a Lock object. It supports loading
|
|
83
|
+
from and saving to disk, appending new messages, clearing histories,
|
|
84
|
+
and truncating histories. Each chat thread is stored in a separate JSON file
|
|
85
|
+
for persistence.
|
|
86
|
+
|
|
87
|
+
Thread safety is crucial to prevent data corruption and race conditions
|
|
88
|
+
when multiple threads access or modify the message histories concurrently.
|
|
89
|
+
|
|
90
|
+
We store two types of messages per thread: AI-optimized and display-optimized messages.
|
|
91
|
+
We store display_history to be able to restore them in the frontend when
|
|
92
|
+
the extension loads. We store ai_optimized_history to keep the conversation context
|
|
93
|
+
for continuing the conversation.
|
|
94
|
+
|
|
95
|
+
The JSON file structure for storing each thread is as follows:
|
|
96
|
+
{
|
|
97
|
+
"chat_history_version": 2,
|
|
98
|
+
"thread_id": "<uuid>",
|
|
99
|
+
"creation_ts": 1234567890.123,
|
|
100
|
+
"last_interaction_ts": 1234567890.123,
|
|
101
|
+
"name": "Short descriptive name",
|
|
102
|
+
"ai_optimized_history": [
|
|
103
|
+
{
|
|
104
|
+
"role": "user",
|
|
105
|
+
"content": "..."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"role": "assistant",
|
|
109
|
+
"content": "..."
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"display_history": [
|
|
113
|
+
{
|
|
114
|
+
"role": "user",
|
|
115
|
+
"content": "..."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"role": "assistant",
|
|
119
|
+
"content": "..."
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Each thread is stored in a separate JSON file named "<thread_id>.json".
|
|
125
|
+
|
|
126
|
+
Attributes:
|
|
127
|
+
_lock (Lock): Ensures thread-safe access.
|
|
128
|
+
_chats_dir (str): Directory where chat thread files are stored.
|
|
129
|
+
_chat_threads (Dict[ThreadID, ChatThread]): In-memory cache of all chat threads.
|
|
130
|
+
|
|
131
|
+
Methods:
|
|
132
|
+
create_new_thread() -> ThreadID:
|
|
133
|
+
Creates a new empty chat thread and returns its ID.
|
|
134
|
+
get_ai_optimized_history(thread_id: Optional[ThreadID] = None) -> List[ChatCompletionMessageParam]:
|
|
135
|
+
Returns the AI-optimized history for the specified thread or newest thread.
|
|
136
|
+
get_display_history(thread_id: Optional[ThreadID] = None) -> List[ChatCompletionMessageParam]:
|
|
137
|
+
Returns the display-optimized history for the specified thread or newest thread.
|
|
138
|
+
append_message(ai_optimized_message: ChatCompletionMessageParam, display_message: ChatCompletionMessageParam, llm_provider: OpenAIProvider, thread_id: Optional[ThreadID] = None) -> None:
|
|
139
|
+
Appends messages to the specified thread (or newest thread) and generates a name if needed.
|
|
140
|
+
truncate_histories(index: int, thread_id: Optional[ThreadID] = None) -> None:
|
|
141
|
+
Truncates messages at the given index for the specified thread.
|
|
142
|
+
delete_thread(thread_id: ThreadID) -> bool:
|
|
143
|
+
Deletes a chat thread by its ID from memory and disk, returns success status.
|
|
144
|
+
get_threads() -> List[ChatThreadMetadata]:
|
|
145
|
+
Returns a list of threads with metadata, sorted by last interaction (newest first).
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self) -> None:
|
|
149
|
+
self._lock = Lock()
|
|
150
|
+
self._chats_dir = os.path.join(MITO_FOLDER, "ai-chats")
|
|
151
|
+
os.makedirs(self._chats_dir, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
# In-memory cache of all chat threads loaded from disk
|
|
154
|
+
self._chat_threads: Dict[ThreadID, ChatThread] = {}
|
|
155
|
+
|
|
156
|
+
# Load existing threads from disk on startup
|
|
157
|
+
self._load_all_threads_from_disk()
|
|
158
|
+
|
|
159
|
+
def create_new_thread(self) -> ThreadID:
|
|
160
|
+
"""
|
|
161
|
+
Creates a new empty chat thread and saves it immediately.
|
|
162
|
+
"""
|
|
163
|
+
with self._lock:
|
|
164
|
+
thread_id = ThreadID(str(uuid.uuid4()))
|
|
165
|
+
now = time.time()
|
|
166
|
+
new_thread = ChatThread(
|
|
167
|
+
thread_id=thread_id,
|
|
168
|
+
creation_ts=now,
|
|
169
|
+
last_interaction_ts=now,
|
|
170
|
+
name=NEW_CHAT_NAME, # we'll fill this in once we have at least user & assistant messages
|
|
171
|
+
)
|
|
172
|
+
self._chat_threads[thread_id] = new_thread
|
|
173
|
+
self._save_thread_to_disk(new_thread)
|
|
174
|
+
return thread_id
|
|
175
|
+
|
|
176
|
+
def _load_all_threads_from_disk(self) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Loads each .json file in `self._chats_dir` into self._chat_threads.
|
|
179
|
+
"""
|
|
180
|
+
files = os.listdir(self._chats_dir)
|
|
181
|
+
for file_name in files:
|
|
182
|
+
if not file_name.endswith(".json"):
|
|
183
|
+
continue
|
|
184
|
+
path = os.path.join(self._chats_dir, file_name)
|
|
185
|
+
try:
|
|
186
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
187
|
+
data = json.load(f)
|
|
188
|
+
|
|
189
|
+
# Check version
|
|
190
|
+
file_version = data.get("chat_history_version", 0)
|
|
191
|
+
if file_version == CHAT_HISTORY_VERSION:
|
|
192
|
+
thread = ChatThread(
|
|
193
|
+
thread_id=ThreadID(data["thread_id"]),
|
|
194
|
+
creation_ts=data["creation_ts"],
|
|
195
|
+
last_interaction_ts=data["last_interaction_ts"],
|
|
196
|
+
name=data["name"],
|
|
197
|
+
ai_optimized_history=data.get("ai_optimized_history", []),
|
|
198
|
+
display_history=data.get("display_history", []),
|
|
199
|
+
)
|
|
200
|
+
self._chat_threads[thread.thread_id] = thread
|
|
201
|
+
else:
|
|
202
|
+
# If versions don't match, throw a warning
|
|
203
|
+
print(
|
|
204
|
+
f"Warning: Incompatible chat history version ({file_version}). "
|
|
205
|
+
f"Expected version {CHAT_HISTORY_VERSION}."
|
|
206
|
+
)
|
|
207
|
+
f.close()
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f"Error loading chat thread from {path}: {e}")
|
|
210
|
+
|
|
211
|
+
def _save_thread_to_disk(self, thread: ChatThread) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Saves the given ChatThread to a JSON file `<thread_id>.json` in `self._chats_dir`.
|
|
214
|
+
"""
|
|
215
|
+
path = os.path.join(self._chats_dir, f"{thread.thread_id}.json")
|
|
216
|
+
|
|
217
|
+
# Using a temporary file and rename for safer "atomic" writes
|
|
218
|
+
tmp_file = path + ".tmp"
|
|
219
|
+
try:
|
|
220
|
+
with open(tmp_file, "w", encoding="utf-8") as f:
|
|
221
|
+
json.dump(thread.__dict__, f, indent=2)
|
|
222
|
+
os.replace(tmp_file, path)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"Error saving chat thread {thread.thread_id}: {e}")
|
|
225
|
+
|
|
226
|
+
def _get_newest_thread_id(self) -> Optional[ThreadID]:
|
|
227
|
+
"""
|
|
228
|
+
Returns the thread_id of the thread with the latest 'last_interaction_ts'.
|
|
229
|
+
If no threads exist, return None.
|
|
230
|
+
"""
|
|
231
|
+
if not self._chat_threads:
|
|
232
|
+
return None
|
|
233
|
+
return max(self._chat_threads, key=lambda tid: self._chat_threads[tid].last_interaction_ts)
|
|
234
|
+
|
|
235
|
+
def _update_last_interaction(self, thread: ChatThread) -> None:
|
|
236
|
+
thread.last_interaction_ts = time.time()
|
|
237
|
+
|
|
238
|
+
def get_ai_optimized_history(self, thread_id: ThreadID) -> List[ChatCompletionMessageParam]:
|
|
239
|
+
"""
|
|
240
|
+
Returns the AI-optimized message history for the specified thread or the newest thread if not specified.
|
|
241
|
+
"""
|
|
242
|
+
with self._lock:
|
|
243
|
+
if thread_id not in self._chat_threads:
|
|
244
|
+
return []
|
|
245
|
+
return self._chat_threads[thread_id].ai_optimized_history
|
|
246
|
+
|
|
247
|
+
def get_display_history(self, thread_id: ThreadID) -> List[ChatCompletionMessageParam]:
|
|
248
|
+
"""
|
|
249
|
+
Returns the display-optimized message history for the specified thread or the newest thread if not specified.
|
|
250
|
+
"""
|
|
251
|
+
with self._lock:
|
|
252
|
+
if thread_id not in self._chat_threads:
|
|
253
|
+
return []
|
|
254
|
+
|
|
255
|
+
thread = self._chat_threads[thread_id]
|
|
256
|
+
display_history = thread.display_history
|
|
257
|
+
|
|
258
|
+
# When we get a thread, update it's last interaction time so that if the
|
|
259
|
+
# user refreshes their browser, this chat will re-appear as the last opened chat.
|
|
260
|
+
self._update_last_interaction(thread)
|
|
261
|
+
self._save_thread_to_disk(thread)
|
|
262
|
+
return display_history
|
|
263
|
+
|
|
264
|
+
async def append_message(
|
|
265
|
+
self,
|
|
266
|
+
ai_optimized_message: ChatCompletionMessageParam,
|
|
267
|
+
display_message: ChatCompletionMessageParam,
|
|
268
|
+
model: str,
|
|
269
|
+
llm_provider: OpenAIProvider,
|
|
270
|
+
thread_id: ThreadID
|
|
271
|
+
) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Appends the messages to the specified thread or the newest thread if not specified.
|
|
274
|
+
If there are no threads yet, create one.
|
|
275
|
+
We also detect if we should set a short name for the thread.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
# Add messages and check if naming is needed while holding the lock
|
|
279
|
+
name_gen_input = None
|
|
280
|
+
with self._lock:
|
|
281
|
+
thread = self._chat_threads[thread_id]
|
|
282
|
+
thread.ai_optimized_history.append(ai_optimized_message)
|
|
283
|
+
thread.display_history.append(display_message)
|
|
284
|
+
self._update_last_interaction(thread)
|
|
285
|
+
|
|
286
|
+
# Trim old messages in ai_optimized_history to reduce token count
|
|
287
|
+
thread.ai_optimized_history = trim_old_messages(thread.ai_optimized_history)
|
|
288
|
+
|
|
289
|
+
if thread.name == NEW_CHAT_NAME and len(thread.display_history) >= 2:
|
|
290
|
+
# Retrieve first user and assistant messages from display_history
|
|
291
|
+
user_message = None
|
|
292
|
+
assistant_message = None
|
|
293
|
+
for msg in thread.display_history:
|
|
294
|
+
if msg["role"] == "user" and user_message is None:
|
|
295
|
+
user_message = msg["content"]
|
|
296
|
+
elif msg["role"] == "assistant" and assistant_message is None:
|
|
297
|
+
assistant_message = msg["content"]
|
|
298
|
+
if user_message and assistant_message:
|
|
299
|
+
break
|
|
300
|
+
if user_message and assistant_message:
|
|
301
|
+
name_gen_input = (user_message, assistant_message)
|
|
302
|
+
|
|
303
|
+
# Save the updated thread to disk
|
|
304
|
+
self._save_thread_to_disk(thread)
|
|
305
|
+
|
|
306
|
+
# Outside the lock, await the name generation if needed
|
|
307
|
+
if name_gen_input:
|
|
308
|
+
new_name = await generate_short_chat_name(str(name_gen_input[0]), str(name_gen_input[1]), model, llm_provider)
|
|
309
|
+
with self._lock:
|
|
310
|
+
# Update the thread's name if still required
|
|
311
|
+
thread = self._chat_threads[thread_id]
|
|
312
|
+
if thread is not None and thread.name == NEW_CHAT_NAME:
|
|
313
|
+
thread.name = new_name
|
|
314
|
+
self._save_thread_to_disk(thread)
|
|
315
|
+
|
|
316
|
+
def truncate_histories(self, index: int, thread_id: ThreadID) -> None:
|
|
317
|
+
"""
|
|
318
|
+
For the newest thread, truncate messages at the given index.
|
|
319
|
+
"""
|
|
320
|
+
if index < 0:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
with self._lock:
|
|
324
|
+
thread = self._chat_threads[thread_id]
|
|
325
|
+
thread.ai_optimized_history = thread.ai_optimized_history[:index]
|
|
326
|
+
thread.display_history = thread.display_history[:index]
|
|
327
|
+
self._update_last_interaction(thread)
|
|
328
|
+
self._save_thread_to_disk(thread)
|
|
329
|
+
|
|
330
|
+
def delete_thread(self, thread_id: ThreadID) -> bool:
|
|
331
|
+
"""
|
|
332
|
+
Deletes a chat thread by its ID. Removes both the in-memory entry and the JSON file.
|
|
333
|
+
Includes safety checks to ensure we're only deleting valid thread files.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
# Safety check: validate thread_id is a properly formatted UUID
|
|
337
|
+
if not thread_id or not isinstance(thread_id, str):
|
|
338
|
+
print(f"Invalid thread_id: {thread_id}")
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
# UUIDs should only contain alphanumeric chars and hyphens
|
|
342
|
+
if not all(c.isalnum() or c == '-' for c in thread_id):
|
|
343
|
+
print(f"Thread ID contains invalid characters: {thread_id}")
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
with self._lock:
|
|
347
|
+
# Remove from in-memory cache if present
|
|
348
|
+
if thread_id in self._chat_threads:
|
|
349
|
+
del self._chat_threads[thread_id]
|
|
350
|
+
|
|
351
|
+
# Construct the file path
|
|
352
|
+
path = os.path.join(self._chats_dir, f"{thread_id}.json")
|
|
353
|
+
|
|
354
|
+
# Security check: ensure path is within the expected directory
|
|
355
|
+
if not os.path.normpath(path).startswith(os.path.normpath(self._chats_dir)):
|
|
356
|
+
print(f"Path traversal attempt detected: {path}")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
# Ensure we're only deleting .json files
|
|
360
|
+
if not path.endswith('.json'):
|
|
361
|
+
print(f"Not a .json file: {path}")
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
# Check if the file exists and is actually a file (not directory)
|
|
365
|
+
if os.path.exists(path):
|
|
366
|
+
if not os.path.isfile(path):
|
|
367
|
+
print(f"Path exists but is not a file: {path}")
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
os.remove(path)
|
|
372
|
+
return True
|
|
373
|
+
except Exception as e:
|
|
374
|
+
print(f"Error deleting thread {thread_id}: {e}")
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
def get_threads(self) -> List[ChatThreadMetadata]:
|
|
380
|
+
"""
|
|
381
|
+
Returns a list of all chat threads with keys:
|
|
382
|
+
- thread_id
|
|
383
|
+
- name
|
|
384
|
+
- creation_ts
|
|
385
|
+
- last_interaction_ts
|
|
386
|
+
The list is sorted by last_interaction_ts (newest first).
|
|
387
|
+
"""
|
|
388
|
+
with self._lock:
|
|
389
|
+
threads = []
|
|
390
|
+
for thread in self._chat_threads.values():
|
|
391
|
+
threads.append(ChatThreadMetadata(
|
|
392
|
+
thread_id=thread.thread_id,
|
|
393
|
+
name=thread.name,
|
|
394
|
+
creation_ts=thread.creation_ts,
|
|
395
|
+
last_interaction_ts=thread.last_interaction_ts,
|
|
396
|
+
))
|
|
397
|
+
threads.sort(key=lambda x: x.last_interaction_ts, reverse=True)
|
|
398
|
+
|
|
399
|
+
# Since we expect vast majority of chats are never going to be deleted,
|
|
400
|
+
# we cut off the list of threads to a reasonable number.
|
|
401
|
+
return threads[:NUMBER_OF_THREADS_CUT_OFF]
|