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,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
|