nc1709 1.15.4__py3-none-any.whl → 1.18.8__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.
@@ -0,0 +1,337 @@
1
+ """
2
+ Dynamic Thinking Messages
3
+ Engaging, varied status messages to keep users informed during processing.
4
+ """
5
+ import random
6
+ import time
7
+ from typing import List, Optional, Tuple
8
+ from enum import Enum
9
+
10
+
11
+ class ThinkingPhase(Enum):
12
+ """Different phases of processing"""
13
+ INITIAL = "initial"
14
+ ANALYZING = "analyzing"
15
+ PLANNING = "planning"
16
+ RESEARCHING = "researching"
17
+ CODING = "coding"
18
+ REVIEWING = "reviewing"
19
+ FINALIZING = "finalizing"
20
+
21
+
22
+ # Dynamic thinking messages organized by phase
23
+ THINKING_MESSAGES = {
24
+ ThinkingPhase.INITIAL: [
25
+ "Let me think about this...",
26
+ "Hmm, interesting question...",
27
+ "Give me a moment to process this...",
28
+ "Let me understand what you need...",
29
+ "Processing your request...",
30
+ "Analyzing your input...",
31
+ "Let me work on this...",
32
+ "I'm on it...",
33
+ "Just a second, thinking...",
34
+ "Let me see what we can do here...",
35
+ ],
36
+ ThinkingPhase.ANALYZING: [
37
+ "Analyzing the codebase...",
38
+ "Looking at the structure...",
39
+ "Understanding the context...",
40
+ "Examining the code...",
41
+ "Digging deeper into this...",
42
+ "Studying the patterns...",
43
+ "Mapping out the architecture...",
44
+ "Tracing the dependencies...",
45
+ "Checking the relationships...",
46
+ "Scanning for relevant code...",
47
+ ],
48
+ ThinkingPhase.PLANNING: [
49
+ "Planning my approach...",
50
+ "Figuring out the best way...",
51
+ "Strategizing the solution...",
52
+ "Breaking this down...",
53
+ "Mapping out the steps...",
54
+ "Designing the approach...",
55
+ "Working out the details...",
56
+ "Thinking through the logic...",
57
+ "Considering the options...",
58
+ "Evaluating different approaches...",
59
+ ],
60
+ ThinkingPhase.RESEARCHING: [
61
+ "Searching for information...",
62
+ "Looking up references...",
63
+ "Finding relevant examples...",
64
+ "Checking documentation...",
65
+ "Gathering context...",
66
+ "Exploring possibilities...",
67
+ "Investigating the best practices...",
68
+ "Reviewing similar patterns...",
69
+ "Looking for the right approach...",
70
+ "Consulting my knowledge...",
71
+ ],
72
+ ThinkingPhase.CODING: [
73
+ "Writing the code...",
74
+ "Crafting the solution...",
75
+ "Building this out...",
76
+ "Implementing the changes...",
77
+ "Putting it together...",
78
+ "Coding away...",
79
+ "Making it happen...",
80
+ "Bringing this to life...",
81
+ "Creating the implementation...",
82
+ "Working on the code...",
83
+ ],
84
+ ThinkingPhase.REVIEWING: [
85
+ "Double-checking my work...",
86
+ "Reviewing the solution...",
87
+ "Making sure everything's right...",
88
+ "Validating the approach...",
89
+ "Checking for issues...",
90
+ "Looking over the code...",
91
+ "Ensuring quality...",
92
+ "Verifying the logic...",
93
+ "Testing my assumptions...",
94
+ "Polishing the solution...",
95
+ ],
96
+ ThinkingPhase.FINALIZING: [
97
+ "Almost there...",
98
+ "Finishing up...",
99
+ "Wrapping things up...",
100
+ "Just about done...",
101
+ "Final touches...",
102
+ "Putting on the finishing touches...",
103
+ "One moment more...",
104
+ "Just finalizing...",
105
+ "Nearly there...",
106
+ "Preparing the response...",
107
+ ],
108
+ }
109
+
110
+ # Engaging idle messages when waiting longer
111
+ LONG_WAIT_MESSAGES = [
112
+ "Still working on it, thanks for your patience...",
113
+ "This is taking a bit longer, but I'm making progress...",
114
+ "Complex task, but I'm getting there...",
115
+ "Bear with me, almost got it...",
116
+ "Working through some tricky parts...",
117
+ "Thorough analysis takes a moment...",
118
+ "Good things take time...",
119
+ "Deep diving into this one...",
120
+ "Making sure I get it right...",
121
+ "Quality takes a moment...",
122
+ ]
123
+
124
+ # Personal/friendly messages for casual interaction
125
+ CASUAL_MESSAGES = [
126
+ "Let me see...",
127
+ "Okay, working on it...",
128
+ "Sure thing, give me a sec...",
129
+ "On it!",
130
+ "Let me figure this out...",
131
+ "Alright, let's do this...",
132
+ "Got it, processing...",
133
+ "Looking into it...",
134
+ "Here we go...",
135
+ "Let me help with that...",
136
+ ]
137
+
138
+ # Tool action messages - what to show when specific tools are being used
139
+ TOOL_MESSAGES = {
140
+ "Read": [
141
+ "Reading {target}...",
142
+ "Looking at {target}...",
143
+ "Examining {target}...",
144
+ "Opening {target}...",
145
+ "Checking {target}...",
146
+ ],
147
+ "Write": [
148
+ "Writing to {target}...",
149
+ "Creating {target}...",
150
+ "Saving {target}...",
151
+ "Updating {target}...",
152
+ ],
153
+ "Edit": [
154
+ "Editing {target}...",
155
+ "Modifying {target}...",
156
+ "Updating {target}...",
157
+ "Changing {target}...",
158
+ ],
159
+ "Bash": [
160
+ "Running command...",
161
+ "Executing...",
162
+ "Processing command...",
163
+ "Working on it...",
164
+ ],
165
+ "Grep": [
166
+ "Searching for patterns...",
167
+ "Looking for matches...",
168
+ "Scanning files...",
169
+ "Finding occurrences...",
170
+ ],
171
+ "Glob": [
172
+ "Finding files...",
173
+ "Searching for files...",
174
+ "Locating files...",
175
+ "Scanning directory...",
176
+ ],
177
+ }
178
+
179
+
180
+ class ThinkingMessageManager:
181
+ """
182
+ Manages dynamic thinking messages to keep users engaged.
183
+
184
+ Tracks time elapsed and switches messages to maintain variety
185
+ and show progress even during long processing.
186
+ """
187
+
188
+ def __init__(self):
189
+ self.start_time: Optional[float] = None
190
+ self.current_phase: ThinkingPhase = ThinkingPhase.INITIAL
191
+ self.last_message: str = ""
192
+ self.last_message_time: float = 0
193
+ self.message_count: int = 0
194
+ self.used_messages: set = set()
195
+
196
+ def start(self) -> None:
197
+ """Start a new thinking session"""
198
+ self.start_time = time.time()
199
+ self.message_count = 0
200
+ self.used_messages.clear()
201
+ self.current_phase = ThinkingPhase.INITIAL
202
+
203
+ def reset(self) -> None:
204
+ """Reset the manager for a new request"""
205
+ self.start()
206
+
207
+ def set_phase(self, phase: ThinkingPhase) -> None:
208
+ """Set the current processing phase"""
209
+ self.current_phase = phase
210
+
211
+ def get_elapsed(self) -> float:
212
+ """Get elapsed time in seconds"""
213
+ if self.start_time is None:
214
+ return 0
215
+ return time.time() - self.start_time
216
+
217
+ def should_update(self, interval: float = 3.0) -> bool:
218
+ """Check if it's time for a new message"""
219
+ now = time.time()
220
+ if now - self.last_message_time >= interval:
221
+ return True
222
+ return False
223
+
224
+ def get_message(self, phase: Optional[ThinkingPhase] = None, casual: bool = False) -> str:
225
+ """
226
+ Get a thinking message for the current state.
227
+
228
+ Args:
229
+ phase: Optional phase override
230
+ casual: Use more casual/friendly messages
231
+
232
+ Returns:
233
+ A contextually appropriate thinking message
234
+ """
235
+ phase = phase or self.current_phase
236
+ elapsed = self.get_elapsed()
237
+
238
+ # After 10+ seconds, add some long-wait messages
239
+ if elapsed > 10 and random.random() < 0.3:
240
+ messages = LONG_WAIT_MESSAGES
241
+ # Use casual messages for variety
242
+ elif casual or (random.random() < 0.2 and self.message_count > 0):
243
+ messages = CASUAL_MESSAGES
244
+ else:
245
+ messages = THINKING_MESSAGES.get(phase, THINKING_MESSAGES[ThinkingPhase.INITIAL])
246
+
247
+ # Try to get an unused message for variety
248
+ available = [m for m in messages if m not in self.used_messages]
249
+ if not available:
250
+ # Reset if we've used all messages
251
+ self.used_messages.clear()
252
+ available = messages
253
+
254
+ message = random.choice(available)
255
+ self.used_messages.add(message)
256
+ self.last_message = message
257
+ self.last_message_time = time.time()
258
+ self.message_count += 1
259
+
260
+ return message
261
+
262
+ def get_tool_message(self, tool_name: str, target: str = "") -> str:
263
+ """
264
+ Get a message for a specific tool action.
265
+
266
+ Args:
267
+ tool_name: Name of the tool being used
268
+ target: Target of the action (file, command, etc.)
269
+
270
+ Returns:
271
+ Formatted tool action message
272
+ """
273
+ templates = TOOL_MESSAGES.get(tool_name, ["Working..."])
274
+ template = random.choice(templates)
275
+
276
+ if "{target}" in template and target:
277
+ # Truncate long targets
278
+ if len(target) > 40:
279
+ target = "..." + target[-37:]
280
+ return template.format(target=target)
281
+ return template
282
+
283
+ def get_progress_message(self) -> str:
284
+ """
285
+ Get a progress-aware message based on elapsed time.
286
+
287
+ Returns:
288
+ Message appropriate for current progress
289
+ """
290
+ elapsed = self.get_elapsed()
291
+
292
+ if elapsed < 2:
293
+ return self.get_message(ThinkingPhase.INITIAL)
294
+ elif elapsed < 5:
295
+ return self.get_message(ThinkingPhase.ANALYZING)
296
+ elif elapsed < 10:
297
+ return self.get_message(ThinkingPhase.PLANNING)
298
+ elif elapsed < 20:
299
+ return self.get_message(ThinkingPhase.CODING)
300
+ elif elapsed < 30:
301
+ return self.get_message(ThinkingPhase.REVIEWING)
302
+ else:
303
+ return self.get_message(ThinkingPhase.FINALIZING)
304
+
305
+
306
+ # Global instance for convenience
307
+ _manager = ThinkingMessageManager()
308
+
309
+
310
+ def start_thinking() -> None:
311
+ """Start a new thinking session"""
312
+ _manager.start()
313
+
314
+
315
+ def get_thinking_message(phase: Optional[ThinkingPhase] = None, casual: bool = False) -> str:
316
+ """Get a dynamic thinking message"""
317
+ return _manager.get_message(phase, casual)
318
+
319
+
320
+ def get_tool_message(tool_name: str, target: str = "") -> str:
321
+ """Get a tool-specific message"""
322
+ return _manager.get_tool_message(tool_name, target)
323
+
324
+
325
+ def get_progress_message() -> str:
326
+ """Get a progress-aware message"""
327
+ return _manager.get_progress_message()
328
+
329
+
330
+ def set_phase(phase: ThinkingPhase) -> None:
331
+ """Set the current thinking phase"""
332
+ _manager.set_phase(phase)
333
+
334
+
335
+ def should_update_message(interval: float = 3.0) -> bool:
336
+ """Check if we should update the thinking message"""
337
+ return _manager.should_update(interval)
nc1709/version_check.py CHANGED
@@ -15,7 +15,7 @@ from packaging import version
15
15
  # Cache settings
16
16
  CACHE_DIR = Path.home() / ".nc1709"
17
17
  CACHE_FILE = CACHE_DIR / "version_cache.json"
18
- CACHE_TTL = 86400 # Check once per day (24 hours in seconds)
18
+ CACHE_TTL = 3600 # Check once per hour (reduced from 24h for faster update notifications)
19
19
 
20
20
 
21
21
  def get_current_version() -> str:
@@ -86,8 +86,12 @@ def check_for_update(force: bool = False) -> Tuple[bool, Optional[str], Optional
86
86
  if not force:
87
87
  cache = load_cache()
88
88
  cache_time = cache.get("timestamp", 0)
89
+ cached_current = cache.get("current_version", "")
89
90
 
90
- if time.time() - cache_time < CACHE_TTL:
91
+ # Invalidate cache if installed version changed (user upgraded/downgraded)
92
+ if cached_current != current:
93
+ force = True
94
+ elif time.time() - cache_time < CACHE_TTL:
91
95
  # Use cached result
92
96
  latest = cache.get("latest_version")
93
97
  if latest:
nc1709/web/server.py CHANGED
@@ -9,12 +9,22 @@ from pathlib import Path
9
9
  from typing import Dict, Any, Optional, List
10
10
  from datetime import datetime
11
11
 
12
- from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Header, Depends
12
+ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Header, Depends, Request
13
13
  from fastapi.staticfiles import StaticFiles
14
14
  from fastapi.responses import HTMLResponse, FileResponse
15
15
  from fastapi.middleware.cors import CORSMiddleware
16
16
  from pydantic import BaseModel
17
17
 
18
+ # Import conversation logger
19
+ try:
20
+ from ..conversation_logger import ConversationLogger
21
+ HAS_CONVERSATION_LOGGER = True
22
+ except ImportError:
23
+ HAS_CONVERSATION_LOGGER = False
24
+
25
+ # Session loggers by client IP
26
+ _session_loggers: Dict[str, ConversationLogger] = {}
27
+
18
28
  # Get the directory where this file is located
19
29
  WEB_DIR = Path(__file__).parent
20
30
  STATIC_DIR = WEB_DIR / "static"
@@ -212,11 +222,12 @@ def create_app() -> FastAPI:
212
222
  @app.get("/api/status")
213
223
  async def get_status():
214
224
  """Get system status"""
225
+ from .. import __version__
215
226
  config = get_config()
216
227
 
217
228
  return {
218
229
  "status": "ok",
219
- "version": "1.0.0",
230
+ "version": __version__,
220
231
  "project": str(Path.cwd()),
221
232
  "memory_enabled": config.get("memory.enabled", False),
222
233
  "timestamp": datetime.now().isoformat()
@@ -543,11 +554,32 @@ def create_app() -> FastAPI:
543
554
  @app.get("/api/remote/status")
544
555
  async def remote_status(authorized: bool = Depends(verify_api_key)):
545
556
  """Check remote API status and available models"""
557
+ from .. import __version__
546
558
  config = get_config()
559
+
560
+ # Count available tools (tools that client can execute locally)
561
+ tools_count = 17 # Default count
562
+ try:
563
+ from ..agent.tools.base import ToolRegistry
564
+ from ..agent.tools.file_tools import register_file_tools
565
+ from ..agent.tools.search_tools import register_search_tools
566
+ from ..agent.tools.bash_tool import register_bash_tools
567
+ from ..agent.tools.web_tools import register_web_tools
568
+
569
+ registry = ToolRegistry()
570
+ register_file_tools(registry)
571
+ register_search_tools(registry)
572
+ register_bash_tools(registry)
573
+ register_web_tools(registry)
574
+ tools_count = len(registry.list_names())
575
+ except ImportError:
576
+ pass
577
+
547
578
  return {
548
579
  "status": "ok",
549
580
  "server": "nc1709",
550
- "version": "1.4.0",
581
+ "version": __version__,
582
+ "tools_count": tools_count,
551
583
  "models": config.get("models", {}),
552
584
  "ollama_url": config.get("ollama.base_url", "http://localhost:11434"),
553
585
  "auth_required": bool(config.get("remote.api_key")),
@@ -760,6 +792,7 @@ def create_app() -> FastAPI:
760
792
  @app.post("/api/remote/agent")
761
793
  async def remote_agent_chat(
762
794
  request: AgentChatRequest,
795
+ http_request: Request,
763
796
  authorized: bool = Depends(verify_api_key)
764
797
  ):
765
798
  """
@@ -772,6 +805,26 @@ def create_app() -> FastAPI:
772
805
  4. Client parses tool calls and executes them LOCALLY
773
806
  5. Client sends tool results back for next iteration
774
807
  """
808
+ # Get client IP for logging
809
+ client_ip = http_request.client.host if http_request.client else "unknown"
810
+ user_agent = http_request.headers.get("user-agent", "unknown")
811
+
812
+ # Get or create logger for this client
813
+ if HAS_CONVERSATION_LOGGER:
814
+ if client_ip not in _session_loggers:
815
+ _session_loggers[client_ip] = ConversationLogger(
816
+ ip_address=client_ip,
817
+ user_agent=user_agent,
818
+ mode="server"
819
+ )
820
+ logger = _session_loggers[client_ip]
821
+
822
+ # Log the last user message if present
823
+ if request.messages:
824
+ last_msg = request.messages[-1]
825
+ if last_msg.get("role") == "user":
826
+ logger.log_user_message(last_msg.get("content", ""), {"cwd": request.cwd})
827
+
775
828
  try:
776
829
  from ..llm_adapter import LLMAdapter
777
830
  from ..prompts.unified_prompt import get_unified_prompt
@@ -788,12 +841,19 @@ def create_app() -> FastAPI:
788
841
  # Get LLM response (just thinking, no execution)
789
842
  response = llm.chat(messages)
790
843
 
844
+ # Log assistant response
845
+ if HAS_CONVERSATION_LOGGER and client_ip in _session_loggers:
846
+ _session_loggers[client_ip].log_assistant_message(response[:2000])
847
+
791
848
  return {
792
849
  "response": response,
793
850
  "timestamp": datetime.now().isoformat(),
794
851
  "type": "agent_response"
795
852
  }
796
853
  except Exception as e:
854
+ # Log error
855
+ if HAS_CONVERSATION_LOGGER and client_ip in _session_loggers:
856
+ _session_loggers[client_ip].log_error(str(e))
797
857
  raise HTTPException(status_code=500, detail=str(e))
798
858
 
799
859
  # =========================================================================