cite-agent 1.3.9__py3-none-any.whl → 1.4.3__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.
- cite_agent/__init__.py +13 -13
- cite_agent/__version__.py +1 -1
- cite_agent/action_first_mode.py +150 -0
- cite_agent/adaptive_providers.py +413 -0
- cite_agent/archive_api_client.py +186 -0
- cite_agent/auth.py +0 -1
- cite_agent/auto_expander.py +70 -0
- cite_agent/cache.py +379 -0
- cite_agent/circuit_breaker.py +370 -0
- cite_agent/citation_network.py +377 -0
- cite_agent/cli.py +8 -16
- cite_agent/cli_conversational.py +113 -3
- cite_agent/confidence_calibration.py +381 -0
- cite_agent/deduplication.py +325 -0
- cite_agent/enhanced_ai_agent.py +689 -371
- cite_agent/error_handler.py +228 -0
- cite_agent/execution_safety.py +329 -0
- cite_agent/full_paper_reader.py +239 -0
- cite_agent/observability.py +398 -0
- cite_agent/offline_mode.py +348 -0
- cite_agent/paper_comparator.py +368 -0
- cite_agent/paper_summarizer.py +420 -0
- cite_agent/pdf_extractor.py +350 -0
- cite_agent/proactive_boundaries.py +266 -0
- cite_agent/quality_gate.py +442 -0
- cite_agent/request_queue.py +390 -0
- cite_agent/response_enhancer.py +257 -0
- cite_agent/response_formatter.py +458 -0
- cite_agent/response_pipeline.py +295 -0
- cite_agent/response_style_enhancer.py +259 -0
- cite_agent/self_healing.py +418 -0
- cite_agent/similarity_finder.py +524 -0
- cite_agent/streaming_ui.py +13 -9
- cite_agent/thinking_blocks.py +308 -0
- cite_agent/tool_orchestrator.py +416 -0
- cite_agent/trend_analyzer.py +540 -0
- cite_agent/unpaywall_client.py +226 -0
- {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/METADATA +15 -1
- cite_agent-1.4.3.dist-info/RECORD +62 -0
- cite_agent-1.3.9.dist-info/RECORD +0 -32
- {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/WHEEL +0 -0
- {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/entry_points.txt +0 -0
- {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/licenses/LICENSE +0 -0
- {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response Formatter - Claude/Cursor-level Response Quality
|
|
3
|
+
Creates well-structured, scannable, professional responses
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResponseFormatter:
|
|
13
|
+
"""
|
|
14
|
+
Formats responses to be Cursor/Claude quality:
|
|
15
|
+
- Scannable (bullets, headers, structure)
|
|
16
|
+
- Progressive disclosure (summary → details)
|
|
17
|
+
- Appropriate tone for context
|
|
18
|
+
- Right amount of information
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def format_file_listing(
|
|
23
|
+
files: List[str],
|
|
24
|
+
query_context: str = "",
|
|
25
|
+
max_display: int = 10
|
|
26
|
+
) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Format file listings clearly and scannably
|
|
29
|
+
|
|
30
|
+
BAD:
|
|
31
|
+
/home/user/project/src/main.py
|
|
32
|
+
/home/user/project/src/utils.py
|
|
33
|
+
... [dumps everything]
|
|
34
|
+
|
|
35
|
+
GOOD:
|
|
36
|
+
I found 47 files. Here are the Python files (8):
|
|
37
|
+
• main.py - Main entry point
|
|
38
|
+
• utils.py - Utility functions
|
|
39
|
+
... (6 more)
|
|
40
|
+
|
|
41
|
+
Want to see all files or filter further?
|
|
42
|
+
"""
|
|
43
|
+
if not files:
|
|
44
|
+
return "No files found matching that criteria."
|
|
45
|
+
|
|
46
|
+
# Determine what type of files these are from context/extensions
|
|
47
|
+
file_type = ResponseFormatter._infer_file_type(files, query_context)
|
|
48
|
+
|
|
49
|
+
# Group files by directory for better organization
|
|
50
|
+
grouped = ResponseFormatter._group_files_by_directory(files)
|
|
51
|
+
|
|
52
|
+
total_count = len(files)
|
|
53
|
+
|
|
54
|
+
# Build response
|
|
55
|
+
parts = []
|
|
56
|
+
|
|
57
|
+
# Summary line
|
|
58
|
+
if file_type:
|
|
59
|
+
parts.append(f"I found {total_count} {file_type}:\n")
|
|
60
|
+
else:
|
|
61
|
+
parts.append(f"I found {total_count} files:\n")
|
|
62
|
+
|
|
63
|
+
# Show files (organized by directory if multiple dirs)
|
|
64
|
+
if len(grouped) == 1:
|
|
65
|
+
# All in one directory - just list files
|
|
66
|
+
dir_path, file_list = list(grouped.items())[0]
|
|
67
|
+
display_files = file_list[:max_display]
|
|
68
|
+
|
|
69
|
+
for file_path in display_files:
|
|
70
|
+
filename = os.path.basename(file_path)
|
|
71
|
+
parts.append(f"• {filename}")
|
|
72
|
+
|
|
73
|
+
remaining = total_count - len(display_files)
|
|
74
|
+
if remaining > 0:
|
|
75
|
+
parts.append(f"... ({remaining} more)\n")
|
|
76
|
+
else:
|
|
77
|
+
# Multiple directories - organize by directory
|
|
78
|
+
shown_count = 0
|
|
79
|
+
for dir_path, file_list in list(grouped.items())[:3]: # Show up to 3 dirs
|
|
80
|
+
parts.append(f"\n**{dir_path}:**")
|
|
81
|
+
for file_path in file_list[:5]: # Show up to 5 files per dir
|
|
82
|
+
filename = os.path.basename(file_path)
|
|
83
|
+
parts.append(f" • {filename}")
|
|
84
|
+
shown_count += 1
|
|
85
|
+
|
|
86
|
+
if shown_count >= max_display:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
if shown_count >= max_display:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
remaining = total_count - shown_count
|
|
93
|
+
if remaining > 0:
|
|
94
|
+
remaining_dirs = len(grouped) - 3
|
|
95
|
+
if remaining_dirs > 0:
|
|
96
|
+
parts.append(f"\n... ({remaining} more files in {remaining_dirs} more directories)")
|
|
97
|
+
else:
|
|
98
|
+
parts.append(f"\n... ({remaining} more files)")
|
|
99
|
+
|
|
100
|
+
# Helpful follow-up
|
|
101
|
+
parts.append(f"\nTotal: {total_count} files")
|
|
102
|
+
|
|
103
|
+
if total_count > max_display:
|
|
104
|
+
parts.append("Want to see all files, or filter further?")
|
|
105
|
+
|
|
106
|
+
return "\n".join(parts)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def format_clarification(
|
|
110
|
+
question: str,
|
|
111
|
+
options: List[str],
|
|
112
|
+
context: str = ""
|
|
113
|
+
) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Format clarification requests clearly
|
|
116
|
+
|
|
117
|
+
BAD:
|
|
118
|
+
"Tell me a bit more about what you're looking for"
|
|
119
|
+
|
|
120
|
+
GOOD:
|
|
121
|
+
Which kind of analysis are you interested in?
|
|
122
|
+
• Revenue analysis
|
|
123
|
+
• Market share comparison
|
|
124
|
+
• Growth trends
|
|
125
|
+
|
|
126
|
+
Let me know what you'd like to focus on.
|
|
127
|
+
"""
|
|
128
|
+
parts = []
|
|
129
|
+
|
|
130
|
+
# Lead-in question
|
|
131
|
+
if context:
|
|
132
|
+
parts.append(f"{context}\n")
|
|
133
|
+
else:
|
|
134
|
+
parts.append(f"{question}\n")
|
|
135
|
+
|
|
136
|
+
# Options as bullets
|
|
137
|
+
for opt in options:
|
|
138
|
+
parts.append(f"• {opt}")
|
|
139
|
+
|
|
140
|
+
# Friendly closing
|
|
141
|
+
parts.append("\nLet me know what you'd like to focus on.")
|
|
142
|
+
|
|
143
|
+
return "\n".join(parts)
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def format_code_explanation(
|
|
147
|
+
code: str,
|
|
148
|
+
file_path: str = "",
|
|
149
|
+
summary: str = "",
|
|
150
|
+
key_points: Optional[List[str]] = None
|
|
151
|
+
) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Format code explanations with progressive disclosure
|
|
154
|
+
|
|
155
|
+
Structure:
|
|
156
|
+
1. Summary (what it does - 1 sentence)
|
|
157
|
+
2. Key points (bullets)
|
|
158
|
+
3. Code sample (if needed)
|
|
159
|
+
4. Follow-up offer
|
|
160
|
+
"""
|
|
161
|
+
parts = []
|
|
162
|
+
|
|
163
|
+
# Summary
|
|
164
|
+
if summary:
|
|
165
|
+
parts.append(f"**Summary:** {summary}\n")
|
|
166
|
+
elif file_path:
|
|
167
|
+
filename = os.path.basename(file_path)
|
|
168
|
+
parts.append(f"Here's **{filename}**:\n")
|
|
169
|
+
|
|
170
|
+
# Key points
|
|
171
|
+
if key_points:
|
|
172
|
+
parts.append("**Key points:**")
|
|
173
|
+
for point in key_points:
|
|
174
|
+
parts.append(f"• {point}")
|
|
175
|
+
parts.append("")
|
|
176
|
+
|
|
177
|
+
# Code (show first 30 lines max unless it's short)
|
|
178
|
+
code_lines = code.split('\n')
|
|
179
|
+
if len(code_lines) <= 40:
|
|
180
|
+
# Show all
|
|
181
|
+
parts.append("```")
|
|
182
|
+
parts.append(code)
|
|
183
|
+
parts.append("```")
|
|
184
|
+
else:
|
|
185
|
+
# Show excerpt
|
|
186
|
+
parts.append("```")
|
|
187
|
+
parts.append('\n'.join(code_lines[:30]))
|
|
188
|
+
parts.append("... (truncated)")
|
|
189
|
+
parts.append("```")
|
|
190
|
+
parts.append(f"\n*Showing first 30 of {len(code_lines)} lines*")
|
|
191
|
+
|
|
192
|
+
# Offer to help more
|
|
193
|
+
parts.append("\nWant me to explain any specific parts or look for issues?")
|
|
194
|
+
|
|
195
|
+
return "\n".join(parts)
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def format_data_table(
|
|
199
|
+
data: List[Dict[str, Any]],
|
|
200
|
+
title: str = "",
|
|
201
|
+
max_rows: int = 10
|
|
202
|
+
) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Format data as readable table
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
**Apple Revenue (Quarterly)**
|
|
208
|
+
|
|
209
|
+
| Quarter | Revenue | Growth |
|
|
210
|
+
|---------|---------|--------|
|
|
211
|
+
| Q4 2023 | $89.5B | +12% |
|
|
212
|
+
| Q3 2023 | $81.8B | +8% |
|
|
213
|
+
"""
|
|
214
|
+
if not data:
|
|
215
|
+
return "No data available."
|
|
216
|
+
|
|
217
|
+
parts = []
|
|
218
|
+
|
|
219
|
+
if title:
|
|
220
|
+
parts.append(f"**{title}**\n")
|
|
221
|
+
|
|
222
|
+
# Get column names from first row
|
|
223
|
+
if data:
|
|
224
|
+
columns = list(data[0].keys())
|
|
225
|
+
|
|
226
|
+
# Create header
|
|
227
|
+
header = "| " + " | ".join(columns) + " |"
|
|
228
|
+
separator = "|" + "|".join(["-" * (len(col) + 2) for col in columns]) + "|"
|
|
229
|
+
|
|
230
|
+
parts.append(header)
|
|
231
|
+
parts.append(separator)
|
|
232
|
+
|
|
233
|
+
# Add rows (limit to max_rows)
|
|
234
|
+
for row in data[:max_rows]:
|
|
235
|
+
row_str = "| " + " | ".join([str(row.get(col, "")) for col in columns]) + " |"
|
|
236
|
+
parts.append(row_str)
|
|
237
|
+
|
|
238
|
+
# Show count if truncated
|
|
239
|
+
if len(data) > max_rows:
|
|
240
|
+
parts.append(f"\n*Showing {max_rows} of {len(data)} rows*")
|
|
241
|
+
parts.append("Want to see more data or a specific time period?")
|
|
242
|
+
|
|
243
|
+
return "\n".join(parts)
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def format_greeting(query: str = "") -> str:
|
|
247
|
+
"""
|
|
248
|
+
Format friendly greeting responses
|
|
249
|
+
|
|
250
|
+
Good greetings:
|
|
251
|
+
- Natural and warm
|
|
252
|
+
- Show availability
|
|
253
|
+
- Hint at capabilities
|
|
254
|
+
- No unnecessary details
|
|
255
|
+
"""
|
|
256
|
+
greetings = [
|
|
257
|
+
"Hi there! I'm ready to help. What can I dig into for you?",
|
|
258
|
+
"Hey! I'm here and ready. What do you need?",
|
|
259
|
+
"Hello! Ready to help with research, data, or code. What's up?",
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
# Simple rotation (could be more sophisticated)
|
|
263
|
+
import hashlib
|
|
264
|
+
index = int(hashlib.md5(query.encode()).hexdigest(), 16) % len(greetings)
|
|
265
|
+
return greetings[index]
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def format_acknowledgment(query: str = "") -> str:
|
|
269
|
+
"""Format thanks/acknowledgment responses"""
|
|
270
|
+
responses = [
|
|
271
|
+
"Happy to help! Feel free to ask anything else.",
|
|
272
|
+
"You're welcome! Let me know if you need anything else.",
|
|
273
|
+
"Glad I could help! I'm here if you need more.",
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
import hashlib
|
|
277
|
+
index = int(hashlib.md5(query.encode()).hexdigest(), 16) % len(responses)
|
|
278
|
+
return responses[index]
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def format_shell_output(
|
|
282
|
+
command: str,
|
|
283
|
+
output: str,
|
|
284
|
+
output_type: str = "generic"
|
|
285
|
+
) -> str:
|
|
286
|
+
"""
|
|
287
|
+
Format shell command output clearly
|
|
288
|
+
|
|
289
|
+
PRINCIPLE: Show RESULTS, not commands (unless asked)
|
|
290
|
+
|
|
291
|
+
BAD:
|
|
292
|
+
$ find . -name "*.py"
|
|
293
|
+
./src/main.py
|
|
294
|
+
./src/utils.py
|
|
295
|
+
[dumps everything]
|
|
296
|
+
|
|
297
|
+
GOOD:
|
|
298
|
+
I found 8 Python files:
|
|
299
|
+
• main.py (in src/)
|
|
300
|
+
• utils.py (in src/)
|
|
301
|
+
... (6 more)
|
|
302
|
+
"""
|
|
303
|
+
if output_type == "file_search":
|
|
304
|
+
# Parse file paths from output
|
|
305
|
+
files = [line.strip() for line in output.split('\n') if line.strip()]
|
|
306
|
+
return ResponseFormatter.format_file_listing(files, "search results")
|
|
307
|
+
|
|
308
|
+
elif output_type == "directory_listing":
|
|
309
|
+
# Format directory contents
|
|
310
|
+
files = [line.strip() for line in output.split('\n') if line.strip()]
|
|
311
|
+
return ResponseFormatter.format_file_listing(files, "directory contents")
|
|
312
|
+
|
|
313
|
+
elif output_type == "current_directory":
|
|
314
|
+
# Just show the path cleanly
|
|
315
|
+
path = output.strip().split('\n')[-1] # Last line is usually the path
|
|
316
|
+
return f"We're in **{path}**"
|
|
317
|
+
|
|
318
|
+
else:
|
|
319
|
+
# Generic output - show key information, not overwhelming details
|
|
320
|
+
lines = output.strip().split('\n')
|
|
321
|
+
|
|
322
|
+
if len(lines) <= 10:
|
|
323
|
+
# Short output - show all
|
|
324
|
+
return output.strip()
|
|
325
|
+
else:
|
|
326
|
+
# Long output - show excerpt
|
|
327
|
+
parts = []
|
|
328
|
+
parts.append('\n'.join(lines[:8]))
|
|
329
|
+
parts.append(f"... ({len(lines) - 8} more lines)")
|
|
330
|
+
parts.append("\nWant to see the full output?")
|
|
331
|
+
return '\n'.join(parts)
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _infer_file_type(files: List[str], context: str) -> str:
|
|
335
|
+
"""Infer what type of files these are"""
|
|
336
|
+
if not files:
|
|
337
|
+
return ""
|
|
338
|
+
|
|
339
|
+
# Check file extensions
|
|
340
|
+
extensions = set()
|
|
341
|
+
for f in files:
|
|
342
|
+
ext = Path(f).suffix.lower()
|
|
343
|
+
if ext:
|
|
344
|
+
extensions.add(ext)
|
|
345
|
+
|
|
346
|
+
# Common patterns
|
|
347
|
+
if extensions == {'.py'}:
|
|
348
|
+
return "Python files"
|
|
349
|
+
elif extensions == {'.js', '.jsx', '.ts', '.tsx'}:
|
|
350
|
+
return "JavaScript/TypeScript files"
|
|
351
|
+
elif extensions == {'.md'}:
|
|
352
|
+
return "Markdown files"
|
|
353
|
+
elif extensions == {'.json'}:
|
|
354
|
+
return "JSON files"
|
|
355
|
+
elif extensions == {'.csv'}:
|
|
356
|
+
return "CSV files"
|
|
357
|
+
elif extensions == {'.txt'}:
|
|
358
|
+
return "text files"
|
|
359
|
+
|
|
360
|
+
# Check for test files
|
|
361
|
+
if any('test' in f.lower() for f in files[:5]):
|
|
362
|
+
return "test files"
|
|
363
|
+
|
|
364
|
+
# Check context
|
|
365
|
+
if 'test' in context.lower():
|
|
366
|
+
return "test files"
|
|
367
|
+
elif 'python' in context.lower():
|
|
368
|
+
return "Python files"
|
|
369
|
+
elif 'config' in context.lower():
|
|
370
|
+
return "configuration files"
|
|
371
|
+
|
|
372
|
+
return "files"
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def _group_files_by_directory(files: List[str]) -> Dict[str, List[str]]:
|
|
376
|
+
"""Group files by their parent directory"""
|
|
377
|
+
grouped = {}
|
|
378
|
+
|
|
379
|
+
for file_path in files:
|
|
380
|
+
dir_path = str(Path(file_path).parent)
|
|
381
|
+
if dir_path == '.':
|
|
382
|
+
dir_path = "(current directory)"
|
|
383
|
+
|
|
384
|
+
if dir_path not in grouped:
|
|
385
|
+
grouped[dir_path] = []
|
|
386
|
+
|
|
387
|
+
grouped[dir_path].append(file_path)
|
|
388
|
+
|
|
389
|
+
return grouped
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def apply_progressive_disclosure(
|
|
393
|
+
content: str,
|
|
394
|
+
content_type: str = "generic",
|
|
395
|
+
max_length: int = 500
|
|
396
|
+
) -> str:
|
|
397
|
+
"""
|
|
398
|
+
Apply progressive disclosure pattern
|
|
399
|
+
Show summary first, offer details
|
|
400
|
+
|
|
401
|
+
Pattern:
|
|
402
|
+
1. Summary (key point)
|
|
403
|
+
2. Details (if short)
|
|
404
|
+
3. Offer more (if long)
|
|
405
|
+
"""
|
|
406
|
+
# If content is short, just return as-is
|
|
407
|
+
if len(content) <= max_length:
|
|
408
|
+
return content
|
|
409
|
+
|
|
410
|
+
# Content is long - apply progressive disclosure
|
|
411
|
+
if content_type == "code":
|
|
412
|
+
lines = content.split('\n')
|
|
413
|
+
summary = '\n'.join(lines[:20])
|
|
414
|
+
total_lines = len(lines)
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
f"{summary}\n\n"
|
|
418
|
+
f"... (showing first 20 of {total_lines} lines)\n\n"
|
|
419
|
+
f"Want to see more, or should I explain specific parts?"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
elif content_type == "data":
|
|
423
|
+
# Show first few items + count
|
|
424
|
+
parts = content.split('\n\n')
|
|
425
|
+
summary = '\n\n'.join(parts[:3])
|
|
426
|
+
total_parts = len(parts)
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
f"{summary}\n\n"
|
|
430
|
+
f"... ({total_parts - 3} more items)\n\n"
|
|
431
|
+
f"Want to see everything, or filter further?"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
else:
|
|
435
|
+
# Generic - show first ~300 chars
|
|
436
|
+
summary = content[:max_length].rsplit('.', 1)[0] + '.'
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
f"{summary}\n\n"
|
|
440
|
+
f"... (truncated for length)\n\n"
|
|
441
|
+
f"Want me to continue or focus on something specific?"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# Convenience functions
|
|
446
|
+
def format_file_list(files: List[str], context: str = "") -> str:
|
|
447
|
+
"""Quick file list formatting"""
|
|
448
|
+
return ResponseFormatter.format_file_listing(files, context)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def format_clarify(question: str, options: List[str]) -> str:
|
|
452
|
+
"""Quick clarification formatting"""
|
|
453
|
+
return ResponseFormatter.format_clarification(question, options)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def format_greeting() -> str:
|
|
457
|
+
"""Quick greeting"""
|
|
458
|
+
return ResponseFormatter.format_greeting()
|