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.
Files changed (44) hide show
  1. cite_agent/__init__.py +13 -13
  2. cite_agent/__version__.py +1 -1
  3. cite_agent/action_first_mode.py +150 -0
  4. cite_agent/adaptive_providers.py +413 -0
  5. cite_agent/archive_api_client.py +186 -0
  6. cite_agent/auth.py +0 -1
  7. cite_agent/auto_expander.py +70 -0
  8. cite_agent/cache.py +379 -0
  9. cite_agent/circuit_breaker.py +370 -0
  10. cite_agent/citation_network.py +377 -0
  11. cite_agent/cli.py +8 -16
  12. cite_agent/cli_conversational.py +113 -3
  13. cite_agent/confidence_calibration.py +381 -0
  14. cite_agent/deduplication.py +325 -0
  15. cite_agent/enhanced_ai_agent.py +689 -371
  16. cite_agent/error_handler.py +228 -0
  17. cite_agent/execution_safety.py +329 -0
  18. cite_agent/full_paper_reader.py +239 -0
  19. cite_agent/observability.py +398 -0
  20. cite_agent/offline_mode.py +348 -0
  21. cite_agent/paper_comparator.py +368 -0
  22. cite_agent/paper_summarizer.py +420 -0
  23. cite_agent/pdf_extractor.py +350 -0
  24. cite_agent/proactive_boundaries.py +266 -0
  25. cite_agent/quality_gate.py +442 -0
  26. cite_agent/request_queue.py +390 -0
  27. cite_agent/response_enhancer.py +257 -0
  28. cite_agent/response_formatter.py +458 -0
  29. cite_agent/response_pipeline.py +295 -0
  30. cite_agent/response_style_enhancer.py +259 -0
  31. cite_agent/self_healing.py +418 -0
  32. cite_agent/similarity_finder.py +524 -0
  33. cite_agent/streaming_ui.py +13 -9
  34. cite_agent/thinking_blocks.py +308 -0
  35. cite_agent/tool_orchestrator.py +416 -0
  36. cite_agent/trend_analyzer.py +540 -0
  37. cite_agent/unpaywall_client.py +226 -0
  38. {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/METADATA +15 -1
  39. cite_agent-1.4.3.dist-info/RECORD +62 -0
  40. cite_agent-1.3.9.dist-info/RECORD +0 -32
  41. {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/WHEEL +0 -0
  42. {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/entry_points.txt +0 -0
  43. {cite_agent-1.3.9.dist-info → cite_agent-1.4.3.dist-info}/licenses/LICENSE +0 -0
  44. {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()