Skip to content

Commit 3fb644f

Browse files
committed
init history mode
1 parent dab1184 commit 3fb644f

25 files changed

Lines changed: 3390 additions & 124 deletions

notebook_intelligence/ai_service_manager.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
from notebook_intelligence.api import ButtonData, ChatModel, EmbeddingModel, InlineCompletionModel, LLMProvider, ChatParticipant, ChatRequest, ChatResponse, CompletionContext, ContextRequest, Host, CompletionContextProvider, MCPPrompt, MCPServer, MarkdownData, NotebookIntelligenceExtension, RegistrationError, TelemetryEvent, TelemetryListener, Tool, Toolset
1313
from notebook_intelligence.base_chat_participant import BaseChatParticipant
1414
from notebook_intelligence.config import NBIConfig
15+
from notebook_intelligence.history_backends import (
16+
HistoryPersistenceBackend,
17+
HistoryPersistenceManager,
18+
MySQLHistoryBackend,
19+
SQLiteHistoryBackend,
20+
)
1521
from notebook_intelligence.github_copilot_chat_participant import GithubCopilotChatParticipant
1622
from notebook_intelligence.claude import CLAUDE_CODE_CHAT_PARTICIPANT_ID, ClaudeCodeChatParticipant, ClaudeCodeInlineCompletionModel, fetch_claude_models, get_claude_models
1723
from notebook_intelligence.llm_providers.github_copilot_llm_provider import GitHubCopilotLLMProvider
@@ -61,6 +67,13 @@ def __init__(self, options: Optional[dict] = None):
6167
self._options.get("feature_policies") or {},
6268
self._options.get("string_overrides") or {},
6369
)
70+
self._history_persistence = HistoryPersistenceManager()
71+
self.register_history_persistence_backend(MySQLHistoryBackend())
72+
self.register_history_persistence_backend(SQLiteHistoryBackend())
73+
self._history_persistence.reconfigure(
74+
self._nbi_config.history_config,
75+
self._nbi_config.history_backend_configs,
76+
)
6477
self._openai_compatible_llm_provider = OpenAICompatibleLLMProvider()
6578
self._litellm_compatible_llm_provider = LiteLLMCompatibleLLMProvider()
6679
self._ollama_llm_provider = OllamaLLMProvider()
@@ -93,6 +106,22 @@ def __init__(self, options: Optional[dict] = None):
93106
def nbi_config(self) -> NBIConfig:
94107
return self._nbi_config
95108

109+
@property
110+
def history_persistence(self) -> HistoryPersistenceManager:
111+
return self._history_persistence
112+
113+
def register_history_persistence_backend(
114+
self, backend: HistoryPersistenceBackend
115+
) -> None:
116+
self._history_persistence.register_backend(backend)
117+
118+
def update_history_persistence(self):
119+
"""Refresh history persistence backend configuration from current config."""
120+
self._history_persistence.reconfigure(
121+
self._nbi_config.history_config,
122+
self._nbi_config.history_backend_configs,
123+
)
124+
96125
@property
97126
def ollama_llm_provider(self) -> OllamaLLMProvider:
98127
return self._ollama_llm_provider

notebook_intelligence/api.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ class ChatRequest:
143143
cancel_token: CancelToken = None
144144
# NEW: Add context for rule evaluation
145145
rule_context: Optional[RuleContext] = None
146+
# Internal conversation id for persistence backends
147+
conversation_id: str = None
146148

147149
@dataclass
148150
class ResponseStreamData:
@@ -312,6 +314,12 @@ def message_id(self) -> str:
312314

313315
def stream(self, data: ResponseStreamData, finish: bool = False) -> None:
314316
raise NotImplementedError
317+
318+
def append_tool_calls(self, tool_calls: list[dict] | None) -> None:
319+
return None
320+
321+
def append_history_message(self, message: dict) -> None:
322+
return None
315323

316324
def finish(self) -> None:
317325
raise NotImplementedError
@@ -636,6 +644,7 @@ async def _tool_call_loop(tool_call_rounds: list):
636644

637645
for choice in tool_response['choices']:
638646
message = choice['message']
647+
response.append_tool_calls(message.get('tool_calls'))
639648
# Some models use 'reasoning', some use 'reasoning_content'
640649
raw_reasoning = message.get('reasoning') or message.get('reasoning_content') or ''
641650

@@ -687,6 +696,12 @@ async def _tool_call_loop(tool_call_rounds: list):
687696
args = tool_call['function']['arguments']
688697
else:
689698
args = fuzzy_json_loads(tool_call['function']['arguments'])
699+
700+
# Persist tool execution START (initial record).
701+
if request.conversation_id:
702+
request.host.history_persistence.log_tool_execution(
703+
tool_call['id'], request.conversation_id, tool_name, args, ""
704+
)
690705

691706
tool_properties = tool_to_call.schema["function"]["parameters"]["properties"]
692707
if type(args) is str:
@@ -713,13 +728,25 @@ async def _tool_call_loop(tool_call_rounds: list):
713728
return
714729

715730
tool_call_response = await tool_to_call.handle_tool_call(request, response, tool_context, args)
731+
732+
# Persist tool execution result.
733+
if request.conversation_id:
734+
request.host.history_persistence.log_tool_execution(
735+
tool_call['id'], request.conversation_id, tool_name, args, str(tool_call_response)
736+
)
737+
# Also log the tool message itself
738+
msg_id = str(uuid.uuid4())
739+
request.host.history_persistence.add_message(
740+
msg_id, request.conversation_id, "tool", str(tool_call_response), tool_call_id=tool_call['id']
741+
)
716742

717743
function_call_result_message = {
718744
"role": "tool",
719745
"content": str(tool_call_response),
720746
"tool_call_id": tool_call['id']
721747
}
722748

749+
response.append_history_message(function_call_result_message)
723750
messages.append(function_call_result_message)
724751

725752
if had_tool_call:
@@ -915,6 +942,9 @@ def register_telemetry_listener(self, listener: TelemetryListener) -> None:
915942
def register_toolset(self, toolset: Toolset) -> None:
916943
raise NotImplementedError
917944

945+
def register_history_persistence_backend(self, backend) -> None:
946+
raise NotImplementedError
947+
918948
@property
919949
def nbi_config(self) -> NBIConfig:
920950
raise NotImplementedError
@@ -971,6 +1001,10 @@ def get_skill_manager(self):
9711001
def websocket_connector(self) -> ThreadSafeWebSocketConnector:
9721002
raise NotImplementedError
9731003

1004+
@property
1005+
def history_persistence(self) -> Any:
1006+
return NotImplementedError
1007+
9741008

9751009
class NotebookIntelligenceExtension:
9761010
@property

notebook_intelligence/config.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import stat
77
import sys
88
import tempfile
9+
import copy
910
from typing import Optional
1011

1112
from notebook_intelligence.feature_flags import (
@@ -230,6 +231,64 @@ def active_rules(self) -> dict:
230231
"""Get dictionary of active rule states (filename -> bool)."""
231232
return self.get('active_rules', {})
232233

234+
@property
235+
def history_backend_configs(self) -> dict:
236+
"""Get history persistence backend configuration by backend id."""
237+
defaults = {
238+
'mysql': {
239+
'host': 'localhost',
240+
'port': 3306,
241+
'user': '',
242+
'password': '',
243+
'database': 'notebook_intelligence'
244+
},
245+
'sqlite': {
246+
'path': os.path.join(self.nbi_user_dir, 'history.sqlite3')
247+
}
248+
}
249+
250+
merged = copy.deepcopy(defaults)
251+
configured = self.get('history_backend_configs', {})
252+
if isinstance(configured, dict):
253+
for backend_id, backend_cfg in configured.items():
254+
if not isinstance(backend_cfg, dict):
255+
continue
256+
merged.setdefault(backend_id, {})
257+
merged[backend_id].update(backend_cfg)
258+
259+
legacy_mysql = self.get('mysql_config', None)
260+
if isinstance(legacy_mysql, dict):
261+
merged['mysql'].update(
262+
{
263+
key: value
264+
for key, value in legacy_mysql.items()
265+
if key != 'enabled'
266+
}
267+
)
268+
return merged
269+
270+
@property
271+
def history_config(self) -> dict:
272+
"""Get chat history storage configuration."""
273+
cfg = self.get('history_config', {})
274+
mode = cfg.get('mode', 'local')
275+
backend = cfg.get('backend', 'sqlite')
276+
if mode in ['mysql', 'sqlite']:
277+
backend = mode
278+
mode = 'persistent'
279+
local_max_messages = cfg.get('local_max_messages', 10)
280+
try:
281+
local_max_messages = int(local_max_messages)
282+
except Exception:
283+
local_max_messages = 10
284+
if local_max_messages < 1:
285+
local_max_messages = 1
286+
return {
287+
'mode': mode if mode in ['persistent', 'local', 'none'] else 'local',
288+
'backend': backend if isinstance(backend, str) and backend else 'sqlite',
289+
'local_max_messages': local_max_messages
290+
}
291+
233292
def set_rule_active(self, filename: str, active: bool):
234293
"""Set the active state of a rule."""
235294
active_rules = self.active_rules.copy()

0 commit comments

Comments
 (0)