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,416 @@
1
+ """
2
+ Tool Orchestration - Intelligent Multi-Tool Chaining
3
+ Chain multiple tools together to solve complex queries
4
+
5
+ This is what separates basic execution from intelligent problem-solving
6
+ """
7
+
8
+ import logging
9
+ import asyncio
10
+ from typing import Dict, Any, List, Optional
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ExecutionMode(Enum):
18
+ """How to execute tools"""
19
+ PARALLEL = "parallel" # Execute simultaneously
20
+ SEQUENTIAL = "sequential" # Execute one after another
21
+ CONDITIONAL = "conditional" # Execute based on previous results
22
+
23
+
24
+ @dataclass
25
+ class ToolStep:
26
+ """A single step in the orchestration plan"""
27
+ step_id: int
28
+ tool_name: str
29
+ tool_params: Dict[str, Any]
30
+ execution_mode: ExecutionMode
31
+ depends_on: Optional[List[int]] = None # Which steps must complete first
32
+ condition: Optional[str] = None # Condition for conditional execution
33
+
34
+
35
+ @dataclass
36
+ class OrchestrationPlan:
37
+ """Complete plan for executing multiple tools"""
38
+ steps: List[ToolStep]
39
+ total_steps: int
40
+ estimated_time: float # seconds
41
+ can_parallelize: bool
42
+
43
+
44
+ class ToolOrchestrator:
45
+ """
46
+ Intelligently orchestrate multiple tool executions
47
+
48
+ Examples:
49
+ - "Compare Apple and Microsoft revenue" → Parallel fetch both, sequential comparison
50
+ - "Find Python files and list their functions" → Sequential (find → analyze)
51
+ - "Search papers on X and Y" → Parallel searches, sequential synthesis
52
+ """
53
+
54
+ @classmethod
55
+ async def create_plan(
56
+ cls,
57
+ query: str,
58
+ context: Dict[str, Any],
59
+ available_tools: List[str]
60
+ ) -> OrchestrationPlan:
61
+ """
62
+ Create an execution plan for the query
63
+
64
+ Args:
65
+ query: User query
66
+ context: Available context
67
+ available_tools: List of available tool names
68
+
69
+ Returns:
70
+ Orchestration plan with steps
71
+ """
72
+ query_lower = query.lower()
73
+ steps = []
74
+
75
+ # Pattern 1: Comparison queries
76
+ if any(word in query_lower for word in ['compare', 'versus', 'vs']):
77
+ plan = cls._plan_comparison(query, available_tools)
78
+ return plan
79
+
80
+ # Pattern 2: Multi-entity search
81
+ if 'and' in query_lower and any(word in query_lower for word in ['search', 'find', 'get']):
82
+ plan = cls._plan_multi_search(query, available_tools)
83
+ return plan
84
+
85
+ # Pattern 3: Sequential analysis
86
+ if any(word in query_lower for word in ['then', 'after that', 'next']):
87
+ plan = cls._plan_sequential_tasks(query, available_tools)
88
+ return plan
89
+
90
+ # Pattern 4: Data aggregation
91
+ if any(word in query_lower for word in ['total', 'sum', 'average', 'aggregate']):
92
+ plan = cls._plan_aggregation(query, available_tools)
93
+ return plan
94
+
95
+ # Default: Single-step plan
96
+ return OrchestrationPlan(
97
+ steps=[ToolStep(
98
+ step_id=1,
99
+ tool_name=cls._infer_primary_tool(query, available_tools),
100
+ tool_params={'query': query},
101
+ execution_mode=ExecutionMode.SEQUENTIAL
102
+ )],
103
+ total_steps=1,
104
+ estimated_time=2.0,
105
+ can_parallelize=False
106
+ )
107
+
108
+ @classmethod
109
+ def _plan_comparison(cls, query: str, available_tools: List[str]) -> OrchestrationPlan:
110
+ """
111
+ Plan for comparison queries
112
+
113
+ Example: "Compare Apple and Microsoft revenue"
114
+ Plan:
115
+ 1. [PARALLEL] Fetch Apple data
116
+ 2. [PARALLEL] Fetch Microsoft data
117
+ 3. [SEQUENTIAL] Compare results
118
+ """
119
+ # Extract entities being compared
120
+ entities = cls._extract_entities_for_comparison(query)
121
+
122
+ steps = []
123
+
124
+ # Step 1 & 2: Fetch data in parallel
125
+ for i, entity in enumerate(entities, 1):
126
+ tool = cls._infer_tool_for_entity(entity, available_tools)
127
+ steps.append(ToolStep(
128
+ step_id=i,
129
+ tool_name=tool,
130
+ tool_params={'query': entity},
131
+ execution_mode=ExecutionMode.PARALLEL
132
+ ))
133
+
134
+ # Step 3: Compare (sequential, depends on previous steps)
135
+ steps.append(ToolStep(
136
+ step_id=len(entities) + 1,
137
+ tool_name='synthesize',
138
+ tool_params={'operation': 'compare'},
139
+ execution_mode=ExecutionMode.SEQUENTIAL,
140
+ depends_on=list(range(1, len(entities) + 1))
141
+ ))
142
+
143
+ return OrchestrationPlan(
144
+ steps=steps,
145
+ total_steps=len(steps),
146
+ estimated_time=3.0, # Parallel saves time
147
+ can_parallelize=True
148
+ )
149
+
150
+ @classmethod
151
+ def _plan_multi_search(cls, query: str, available_tools: List[str]) -> OrchestrationPlan:
152
+ """
153
+ Plan for multiple parallel searches
154
+
155
+ Example: "Search for papers on quantum computing and machine learning"
156
+ Plan:
157
+ 1. [PARALLEL] Search quantum computing
158
+ 2. [PARALLEL] Search machine learning
159
+ 3. [SEQUENTIAL] Synthesize results
160
+ """
161
+ # Extract search terms
162
+ terms = cls._extract_search_terms(query)
163
+
164
+ steps = []
165
+
166
+ # Parallel search steps
167
+ for i, term in enumerate(terms, 1):
168
+ steps.append(ToolStep(
169
+ step_id=i,
170
+ tool_name='archive_search',
171
+ tool_params={'query': term},
172
+ execution_mode=ExecutionMode.PARALLEL
173
+ ))
174
+
175
+ # Synthesis step
176
+ steps.append(ToolStep(
177
+ step_id=len(terms) + 1,
178
+ tool_name='synthesize',
179
+ tool_params={'operation': 'combine'},
180
+ execution_mode=ExecutionMode.SEQUENTIAL,
181
+ depends_on=list(range(1, len(terms) + 1))
182
+ ))
183
+
184
+ return OrchestrationPlan(
185
+ steps=steps,
186
+ total_steps=len(steps),
187
+ estimated_time=4.0,
188
+ can_parallelize=True
189
+ )
190
+
191
+ @classmethod
192
+ def _plan_sequential_tasks(cls, query: str, available_tools: List[str]) -> OrchestrationPlan:
193
+ """
194
+ Plan for sequential tasks
195
+
196
+ Example: "Find Python files then analyze them for bugs"
197
+ Plan:
198
+ 1. [SEQUENTIAL] Find Python files
199
+ 2. [SEQUENTIAL] Analyze files (depends on step 1)
200
+ """
201
+ # Parse sequential steps from query
202
+ parts = query.lower().split('then')
203
+
204
+ steps = []
205
+
206
+ for i, part in enumerate(parts, 1):
207
+ tool = cls._infer_primary_tool(part.strip(), available_tools)
208
+ depends_on = [i-1] if i > 1 else None
209
+
210
+ steps.append(ToolStep(
211
+ step_id=i,
212
+ tool_name=tool,
213
+ tool_params={'query': part.strip()},
214
+ execution_mode=ExecutionMode.SEQUENTIAL,
215
+ depends_on=depends_on
216
+ ))
217
+
218
+ return OrchestrationPlan(
219
+ steps=steps,
220
+ total_steps=len(steps),
221
+ estimated_time=len(steps) * 2.0,
222
+ can_parallelize=False
223
+ )
224
+
225
+ @classmethod
226
+ def _plan_aggregation(cls, query: str, available_tools: List[str]) -> OrchestrationPlan:
227
+ """
228
+ Plan for data aggregation queries
229
+
230
+ Example: "Get total revenue for Apple for all quarters"
231
+ Plan:
232
+ 1. [PARALLEL] Fetch Q1 data
233
+ 2. [PARALLEL] Fetch Q2 data
234
+ 3. [PARALLEL] Fetch Q3 data
235
+ 4. [PARALLEL] Fetch Q4 data
236
+ 5. [SEQUENTIAL] Aggregate results
237
+ """
238
+ # Identify what needs aggregation
239
+ # For now, simple plan
240
+ return OrchestrationPlan(
241
+ steps=[
242
+ ToolStep(
243
+ step_id=1,
244
+ tool_name='finsight_search',
245
+ tool_params={'query': query},
246
+ execution_mode=ExecutionMode.SEQUENTIAL
247
+ )
248
+ ],
249
+ total_steps=1,
250
+ estimated_time=2.0,
251
+ can_parallelize=False
252
+ )
253
+
254
+ @classmethod
255
+ async def execute_plan(
256
+ cls,
257
+ plan: OrchestrationPlan,
258
+ tool_executor: Any # The agent that can execute tools
259
+ ) -> Dict[str, Any]:
260
+ """
261
+ Execute the orchestration plan
262
+
263
+ Args:
264
+ plan: The orchestration plan
265
+ tool_executor: Object with methods to execute tools
266
+
267
+ Returns:
268
+ Results from all steps
269
+ """
270
+ results = {}
271
+ pending_steps = plan.steps.copy()
272
+
273
+ while pending_steps:
274
+ # Find steps that can execute now (dependencies met)
275
+ ready_steps = [
276
+ step for step in pending_steps
277
+ if not step.depends_on or all(dep in results for dep in step.depends_on)
278
+ ]
279
+
280
+ if not ready_steps:
281
+ logger.error("Orchestration deadlock - no steps can execute")
282
+ break
283
+
284
+ # Group by execution mode
285
+ parallel_steps = [s for s in ready_steps if s.execution_mode == ExecutionMode.PARALLEL]
286
+ sequential_steps = [s for s in ready_steps if s.execution_mode == ExecutionMode.SEQUENTIAL]
287
+
288
+ # Execute parallel steps
289
+ if parallel_steps:
290
+ tasks = []
291
+ for step in parallel_steps:
292
+ task = cls._execute_tool(step, tool_executor, results)
293
+ tasks.append((step.step_id, task))
294
+
295
+ # Wait for all parallel tasks
296
+ for step_id, task in tasks:
297
+ result = await task
298
+ results[step_id] = result
299
+
300
+ # Remove completed steps
301
+ for step in parallel_steps:
302
+ pending_steps.remove(step)
303
+
304
+ # Execute sequential steps (one at a time)
305
+ for step in sequential_steps:
306
+ result = await cls._execute_tool(step, tool_executor, results)
307
+ results[step.step_id] = result
308
+ pending_steps.remove(step)
309
+
310
+ return results
311
+
312
+ @classmethod
313
+ async def _execute_tool(
314
+ cls,
315
+ step: ToolStep,
316
+ tool_executor: Any,
317
+ previous_results: Dict[int, Any]
318
+ ) -> Any:
319
+ """Execute a single tool step"""
320
+ # Get dependencies if needed
321
+ dependencies = {}
322
+ if step.depends_on:
323
+ dependencies = {
324
+ dep_id: previous_results[dep_id]
325
+ for dep_id in step.depends_on
326
+ if dep_id in previous_results
327
+ }
328
+
329
+ # Execute the tool
330
+ logger.info(f"Executing step {step.step_id}: {step.tool_name}")
331
+
332
+ # Call the appropriate method on tool_executor
333
+ if hasattr(tool_executor, step.tool_name):
334
+ method = getattr(tool_executor, step.tool_name)
335
+ result = await method(**step.tool_params, dependencies=dependencies)
336
+ else:
337
+ logger.warning(f"Tool {step.tool_name} not found")
338
+ result = {"error": f"Tool {step.tool_name} not available"}
339
+
340
+ return result
341
+
342
+ # Helper methods for parsing queries
343
+
344
+ @classmethod
345
+ def _extract_entities_for_comparison(cls, query: str) -> List[str]:
346
+ """Extract entities being compared"""
347
+ # Simple extraction (could be more sophisticated)
348
+ query_lower = query.lower()
349
+
350
+ # Remove comparison words
351
+ for word in ['compare', 'versus', 'vs', 'vs.', 'and']:
352
+ query_lower = query_lower.replace(word, ' ')
353
+
354
+ # Split and clean
355
+ parts = [p.strip() for p in query_lower.split() if len(p.strip()) > 2]
356
+
357
+ # Return first two meaningful terms (could be improved)
358
+ return parts[:2] if len(parts) >= 2 else parts
359
+
360
+ @classmethod
361
+ def _extract_search_terms(cls, query: str) -> List[str]:
362
+ """Extract multiple search terms from query"""
363
+ # Split on 'and'
364
+ if ' and ' in query.lower():
365
+ parts = query.lower().split(' and ')
366
+ return [p.strip() for p in parts if len(p.strip()) > 3]
367
+
368
+ return [query]
369
+
370
+ @classmethod
371
+ def _infer_primary_tool(cls, query: str, available_tools: List[str]) -> str:
372
+ """Infer which tool to use for a query"""
373
+ query_lower = query.lower()
374
+
375
+ if any(word in query_lower for word in ['paper', 'research', 'study']):
376
+ return 'archive_search'
377
+
378
+ if any(word in query_lower for word in ['revenue', 'stock', 'company', 'financial']):
379
+ return 'finsight_search'
380
+
381
+ if any(word in query_lower for word in ['file', 'directory', 'code']):
382
+ return 'shell_command'
383
+
384
+ return 'web_search'
385
+
386
+ @classmethod
387
+ def _infer_tool_for_entity(cls, entity: str, available_tools: List[str]) -> str:
388
+ """Infer which tool to use for a specific entity"""
389
+ # Check if it's a company name or financial term
390
+ # For now, simple heuristic
391
+ return 'finsight_search'
392
+
393
+
394
+ # Convenience function
395
+ async def orchestrate_tools(query: str, context: Dict[str, Any], agent: Any) -> Dict[str, Any]:
396
+ """
397
+ Quick tool orchestration
398
+
399
+ Args:
400
+ query: User query
401
+ context: Available context
402
+ agent: Agent object that can execute tools
403
+
404
+ Returns:
405
+ Results from orchestrated execution
406
+ """
407
+ available_tools = [
408
+ 'archive_search', 'finsight_search', 'shell_command',
409
+ 'web_search', 'synthesize'
410
+ ]
411
+
412
+ plan = await ToolOrchestrator.create_plan(query, context, available_tools)
413
+ logger.info(f"Created orchestration plan with {plan.total_steps} steps")
414
+
415
+ results = await ToolOrchestrator.execute_plan(plan, agent)
416
+ return results