Sujatha commited on
Commit
f3d5e4a
·
verified ·
1 Parent(s): 8158ef6

Create agents / calculator_agent.py

Browse files
Files changed (1) hide show
  1. agents / calculator_agent.py +539 -0
agents / calculator_agent.py ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Calculator Agent - Performs complex financial calculations
3
+ """
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from typing import Dict, List, Any, Optional, Union
8
+ from datetime import datetime
9
+ import re
10
+ import math
11
+
12
+
13
+ class CalculatorAgent:
14
+ """Performs complex financial and quantitative calculations"""
15
+
16
+ def __init__(self):
17
+ self.calculation_history = []
18
+ self.formulas = self._load_formulas()
19
+
20
+ def _load_formulas(self) -> Dict[str, Dict]:
21
+ """Load financial formulas and their implementations"""
22
+ return {
23
+ 'future_value': {
24
+ 'description': 'Future value of a single cash flow',
25
+ 'formula': 'FV = PV × (1 + r)^n',
26
+ 'parameters': ['PV', 'r', 'n'],
27
+ 'function': self._calculate_fv
28
+ },
29
+ 'present_value': {
30
+ 'description': 'Present value of a single cash flow',
31
+ 'formula': 'PV = FV / (1 + r)^n',
32
+ 'parameters': ['FV', 'r', 'n'],
33
+ 'function': self._calculate_pv
34
+ },
35
+ 'annuity_pv': {
36
+ 'description': 'Present value of an ordinary annuity',
37
+ 'formula': 'PV = PMT × [1 - (1 + r)^-n] / r',
38
+ 'parameters': ['PMT', 'r', 'n'],
39
+ 'function': self._calculate_annuity_pv
40
+ },
41
+ 'annuity_fv': {
42
+ 'description': 'Future value of an ordinary annuity',
43
+ 'formula': 'FV = PMT × [(1 + r)^n - 1] / r',
44
+ 'parameters': ['PMT', 'r', 'n'],
45
+ 'function': self._calculate_annuity_fv
46
+ },
47
+ 'perpetuity_pv': {
48
+ 'description': 'Present value of a perpetuity',
49
+ 'formula': 'PV = PMT / r',
50
+ 'parameters': ['PMT', 'r'],
51
+ 'function': self._calculate_perpetuity_pv
52
+ },
53
+ 'npv': {
54
+ 'description': 'Net present value of cash flows',
55
+ 'formula': 'NPV = Σ [CF_t / (1 + r)^t]',
56
+ 'parameters': ['cash_flows', 'r'],
57
+ 'function': self._calculate_npv
58
+ },
59
+ 'irr': {
60
+ 'description': 'Internal rate of return',
61
+ 'formula': '0 = Σ [CF_t / (1 + IRR)^t]',
62
+ 'parameters': ['cash_flows'],
63
+ 'function': self._calculate_irr
64
+ },
65
+ 'mean': {
66
+ 'description': 'Arithmetic mean',
67
+ 'formula': 'μ = Σ x_i / n',
68
+ 'parameters': ['data'],
69
+ 'function': self._calculate_mean
70
+ },
71
+ 'variance': {
72
+ 'description': 'Variance',
73
+ 'formula': 'σ² = Σ (x_i - μ)² / (n-1)',
74
+ 'parameters': ['data'],
75
+ 'function': self._calculate_variance
76
+ },
77
+ 'standard_deviation': {
78
+ 'description': 'Standard deviation',
79
+ 'formula': 'σ = √Variance',
80
+ 'parameters': ['data'],
81
+ 'function': self._calculate_std_dev
82
+ },
83
+ 'correlation': {
84
+ 'description': 'Correlation coefficient',
85
+ 'formula': 'ρ = Cov(X,Y) / (σ_X × σ_Y)',
86
+ 'parameters': ['x', 'y'],
87
+ 'function': self._calculate_correlation
88
+ },
89
+ 'covariance': {
90
+ 'description': 'Covariance',
91
+ 'formula': 'Cov(X,Y) = Σ [(x_i - μ_x)(y_i - μ_y)] / (n-1)',
92
+ 'parameters': ['x', 'y'],
93
+ 'function': self._calculate_covariance
94
+ }
95
+ }
96
+
97
+ def process(self,
98
+ query: str,
99
+ user_profile: Dict[str, Any],
100
+ conversation_history: List[Dict] = None) -> Dict[str, Any]:
101
+ """
102
+ Parse and execute calculation requests
103
+
104
+ Args:
105
+ query: Calculation request
106
+ user_profile: User's learning profile
107
+ conversation_history: Previous conversation
108
+
109
+ Returns:
110
+ Calculation results
111
+ """
112
+ # Extract calculation type and parameters
113
+ calc_type, params = self._parse_calculation_request(query)
114
+
115
+ if not calc_type:
116
+ return {
117
+ 'response': "I couldn't identify a specific calculation in your query. "
118
+ "Please specify what you want to calculate (e.g., 'Calculate NPV', "
119
+ "'What is the future value?').",
120
+ 'confidence': 0.3,
121
+ 'metadata': {'parsed': False}
122
+ }
123
+
124
+ try:
125
+ # Perform calculation
126
+ result = self._perform_calculation(calc_type, params)
127
+
128
+ # Format response
129
+ response = self._format_calculation_response(calc_type, params, result)
130
+
131
+ # Log calculation
132
+ self._log_calculation(calc_type, params, result, user_profile)
133
+
134
+ return {
135
+ 'response': response,
136
+ 'result': result,
137
+ 'calculation_type': calc_type,
138
+ 'confidence': 0.95,
139
+ 'metadata': {
140
+ 'formula': self.formulas[calc_type]['formula'],
141
+ 'parameters_used': params
142
+ }
143
+ }
144
+
145
+ except Exception as e:
146
+ error_response = f"Error in calculation: {str(e)}\n\n"
147
+ error_response += f"Formula: {self.formulas[calc_type]['formula']}\n"
148
+ error_response += f"Required parameters: {', '.join(self.formulas[calc_type]['parameters'])}"
149
+
150
+ return {
151
+ 'response': error_response,
152
+ 'error': str(e),
153
+ 'calculation_type': calc_type,
154
+ 'confidence': 0.1,
155
+ 'metadata': {'error': True}
156
+ }
157
+
158
+ def _parse_calculation_request(self, query: str) -> tuple:
159
+ """Parse calculation request to identify type and parameters"""
160
+ query_lower = query.lower()
161
+
162
+ # Map keywords to calculation types
163
+ keyword_mapping = {
164
+ 'future value': 'future_value',
165
+ 'fv': 'future_value',
166
+ 'present value': 'present_value',
167
+ 'pv': 'present_value',
168
+ 'annuity': 'annuity_pv',
169
+ 'perpetuity': 'perpetuity_pv',
170
+ 'npv': 'npv',
171
+ 'net present value': 'npv',
172
+ 'irr': 'irr',
173
+ 'internal rate of return': 'irr',
174
+ 'mean': 'mean',
175
+ 'average': 'mean',
176
+ 'variance': 'variance',
177
+ 'standard deviation': 'standard_deviation',
178
+ 'std dev': 'standard_deviation',
179
+ 'correlation': 'correlation',
180
+ 'covariance': 'covariance',
181
+ 'calculate': None, # Generic - need to infer
182
+ 'compute': None,
183
+ 'solve': None
184
+ }
185
+
186
+ # Identify calculation type
187
+ calc_type = None
188
+ for keyword, ctype in keyword_mapping.items():
189
+ if keyword in query_lower:
190
+ calc_type = ctype
191
+ break
192
+
193
+ # If generic calculation term, try to infer from context
194
+ if not calc_type:
195
+ # Look for formula components
196
+ if any(term in query_lower for term in ['(1+r)', '^n', 'discount']):
197
+ calc_type = 'future_value'
198
+ elif 'cash flow' in query_lower:
199
+ calc_type = 'npv'
200
+
201
+ # Extract parameters
202
+ params = self._extract_parameters(query, calc_type)
203
+
204
+ return calc_type, params
205
+
206
+ def _extract_parameters(self, query: str, calc_type: str) -> Dict[str, Any]:
207
+ """Extract parameters from query text"""
208
+ params = {}
209
+
210
+ # Extract numbers with context
211
+ number_pattern = r'(\d+\.?\d*)'
212
+ numbers = re.findall(number_pattern, query)
213
+
214
+ # Extract percentages
215
+ percent_pattern = r'(\d+\.?\d*)\s*%'
216
+ percents = re.findall(percent_pattern, query)
217
+
218
+ # Extract monetary values
219
+ money_pattern = r'\$(\d+\.?\d*)'
220
+ money = re.findall(money_pattern, query)
221
+
222
+ # Map based on calculation type
223
+ if calc_type == 'future_value':
224
+ if money:
225
+ params['PV'] = float(money[0])
226
+ if percents:
227
+ params['r'] = float(percents[0]) / 100
228
+ if numbers and 'n' not in params:
229
+ # Assume last number is n if not already assigned
230
+ for num in reversed(numbers):
231
+ try:
232
+ if float(num) != params.get('PV', 0) and float(num) != params.get('r', 0)*100:
233
+ params['n'] = float(num)
234
+ break
235
+ except:
236
+ pass
237
+
238
+ elif calc_type == 'present_value':
239
+ if money:
240
+ params['FV'] = float(money[0])
241
+ if percents:
242
+ params['r'] = float(percents[0]) / 100
243
+ if numbers and 'n' not in params:
244
+ for num in reversed(numbers):
245
+ try:
246
+ if float(num) != params.get('FV', 0) and float(num) != params.get('r', 0)*100:
247
+ params['n'] = float(num)
248
+ break
249
+ except:
250
+ pass
251
+
252
+ elif calc_type in ['annuity_pv', 'annuity_fv']:
253
+ if money:
254
+ params['PMT'] = float(money[0])
255
+ if percents:
256
+ params['r'] = float(percents[0]) / 100
257
+ if numbers and 'n' not in params:
258
+ for num in reversed(numbers):
259
+ try:
260
+ if float(num) != params.get('PMT', 0) and float(num) != params.get('r', 0)*100:
261
+ params['n'] = float(num)
262
+ break
263
+ except:
264
+ pass
265
+
266
+ elif calc_type == 'npv':
267
+ # Try to extract cash flow array
268
+ cf_pattern = r'cash flows?\s*[:=]\s*\[([-\d\s,\.]+)\]'
269
+ cf_match = re.search(cf_pattern, query, re.IGNORECASE)
270
+ if cf_match:
271
+ cash_flows_str = cf_match.group(1)
272
+ params['cash_flows'] = [float(x.strip()) for x in cash_flows_str.split(',')]
273
+
274
+ if percents:
275
+ params['r'] = float(percents[0]) / 100
276
+
277
+ elif calc_type in ['mean', 'variance', 'standard_deviation']:
278
+ # Try to extract data array
279
+ data_pattern = r'data\s*[:=]\s*\[([\d\s,\.]+)\]'
280
+ data_match = re.search(data_pattern, query, re.IGNORECASE)
281
+ if data_match:
282
+ data_str = data_match.group(1)
283
+ params['data'] = [float(x.strip()) for x in data_str.split(',')]
284
+
285
+ elif calc_type in ['correlation', 'covariance']:
286
+ # Try to extract two data arrays
287
+ x_pattern = r'x\s*[:=]\s*\[([\d\s,\.]+)\]'
288
+ y_pattern = r'y\s*[:=]\s*\[([\d\s,\.]+)\]'
289
+
290
+ x_match = re.search(x_pattern, query, re.IGNORECASE)
291
+ y_match = re.search(y_pattern, query, re.IGNORECASE)
292
+
293
+ if x_match and y_match:
294
+ x_str = x_match.group(1)
295
+ y_str = y_match.group(1)
296
+ params['x'] = [float(x.strip()) for x in x_str.split(',')]
297
+ params['y'] = [float(y.strip()) for y in y_str.split(',')]
298
+
299
+ return params
300
+
301
+ def _perform_calculation(self, calc_type: str, params: Dict[str, Any]) -> Dict[str, Any]:
302
+ """Perform the actual calculation"""
303
+ formula_info = self.formulas[calc_type]
304
+ calculation_func = formula_info['function']
305
+
306
+ result = calculation_func(params)
307
+
308
+ return {
309
+ 'value': result,
310
+ 'formula': formula_info['formula'],
311
+ 'description': formula_info['description'],
312
+ 'parameters': params
313
+ }
314
+
315
+ # Calculation implementations
316
+ def _calculate_fv(self, params: Dict) -> float:
317
+ """Calculate future value: FV = PV × (1 + r)^n"""
318
+ pv = params.get('PV', 0)
319
+ r = params.get('r', 0)
320
+ n = params.get('n', 0)
321
+ return pv * (1 + r) ** n
322
+
323
+ def _calculate_pv(self, params: Dict) -> float:
324
+ """Calculate present value: PV = FV / (1 + r)^n"""
325
+ fv = params.get('FV', 0)
326
+ r = params.get('r', 0)
327
+ n = params.get('n', 0)
328
+ return fv / (1 + r) ** n
329
+
330
+ def _calculate_annuity_pv(self, params: Dict) -> float:
331
+ """Calculate present value of annuity: PV = PMT × [1 - (1 + r)^-n] / r"""
332
+ pmt = params.get('PMT', 0)
333
+ r = params.get('r', 0)
334
+ n = params.get('n', 0)
335
+
336
+ if r == 0:
337
+ return pmt * n
338
+ return pmt * (1 - (1 + r) ** -n) / r
339
+
340
+ def _calculate_annuity_fv(self, params: Dict) -> float:
341
+ """Calculate future value of annuity: FV = PMT × [(1 + r)^n - 1] / r"""
342
+ pmt = params.get('PMT', 0)
343
+ r = params.get('r', 0)
344
+ n = params.get('n', 0)
345
+
346
+ if r == 0:
347
+ return pmt * n
348
+ return pmt * ((1 + r) ** n - 1) / r
349
+
350
+ def _calculate_perpetuity_pv(self, params: Dict) -> float:
351
+ """Calculate present value of perpetuity: PV = PMT / r"""
352
+ pmt = params.get('PMT', 0)
353
+ r = params.get('r', 0)
354
+
355
+ if r == 0:
356
+ raise ValueError("Discount rate cannot be zero for perpetuity")
357
+ return pmt / r
358
+
359
+ def _calculate_npv(self, params: Dict) -> float:
360
+ """Calculate net present value: NPV = Σ [CF_t / (1 + r)^t]"""
361
+ cash_flows = params.get('cash_flows', [])
362
+ r = params.get('r', 0.1)
363
+
364
+ npv = 0
365
+ for t, cf in enumerate(cash_flows):
366
+ npv += cf / (1 + r) ** t
367
+ return npv
368
+
369
+ def _calculate_irr(self, params: Dict) -> float:
370
+ """Calculate internal rate of return"""
371
+ cash_flows = params.get('cash_flows', [])
372
+
373
+ # Simple IRR calculation using numpy's IRR function
374
+ # In production, use numpy.irr or a more robust implementation
375
+ if len(cash_flows) < 2:
376
+ raise ValueError("Need at least two cash flows for IRR calculation")
377
+
378
+ # Simple implementation for demonstration
379
+ # Actual implementation would use numerical methods
380
+ def npv_func(rate):
381
+ return sum(cf / (1 + rate) ** i for i, cf in enumerate(cash_flows))
382
+
383
+ # Use binary search to find IRR
384
+ lower, upper = -0.99, 10.0
385
+ for _ in range(100):
386
+ mid = (lower + upper) / 2
387
+ if npv_func(mid) > 0:
388
+ lower = mid
389
+ else:
390
+ upper = mid
391
+
392
+ return (lower + upper) / 2
393
+
394
+ def _calculate_mean(self, params: Dict) -> float:
395
+ """Calculate mean: μ = Σ x_i / n"""
396
+ data = params.get('data', [])
397
+ if not data:
398
+ raise ValueError("No data provided")
399
+ return sum(data) / len(data)
400
+
401
+ def _calculate_variance(self, params: Dict) -> float:
402
+ """Calculate sample variance: σ² = Σ (x_i - μ)² / (n-1)"""
403
+ data = params.get('data', [])
404
+ if len(data) < 2:
405
+ raise ValueError("Need at least 2 data points for variance")
406
+
407
+ mean = self._calculate_mean({'data': data})
408
+ squared_diffs = [(x - mean) ** 2 for x in data]
409
+ return sum(squared_diffs) / (len(data) - 1)
410
+
411
+ def _calculate_std_dev(self, params: Dict) -> float:
412
+ """Calculate standard deviation: σ = √Variance"""
413
+ variance = self._calculate_variance(params)
414
+ return math.sqrt(variance)
415
+
416
+ def _calculate_covariance(self, params: Dict) -> float:
417
+ """Calculate covariance: Cov(X,Y) = Σ [(x_i - μ_x)(y_i - μ_y)] / (n-1)"""
418
+ x = params.get('x', [])
419
+ y = params.get('y', [])
420
+
421
+ if len(x) != len(y):
422
+ raise ValueError("X and Y must have same length")
423
+ if len(x) < 2:
424
+ raise ValueError("Need at least 2 data points")
425
+
426
+ mean_x = self._calculate_mean({'data': x})
427
+ mean_y = self._calculate_mean({'data': y})
428
+
429
+ cov_sum = sum((x[i] - mean_x) * (y[i] - mean_y) for i in range(len(x)))
430
+ return cov_sum / (len(x) - 1)
431
+
432
+ def _calculate_correlation(self, params: Dict) -> float:
433
+ """Calculate correlation: ρ = Cov(X,Y) / (σ_X × σ_Y)"""
434
+ cov = self._calculate_covariance(params)
435
+ std_x = self._calculate_std_dev({'data': params.get('x', [])})
436
+ std_y = self._calculate_std_dev({'data': params.get('y', [])})
437
+
438
+ if std_x == 0 or std_y == 0:
439
+ return 0
440
+ return cov / (std_x * std_y)
441
+
442
+ def _format_calculation_response(self, calc_type: str, params: Dict, result: Dict) -> str:
443
+ """Format calculation results for display"""
444
+ formula_info = self.formulas[calc_type]
445
+
446
+ response = f"## 🧮 Calculation Result: {formula_info['description']}\n\n"
447
+ response += f"**Formula:** `{formula_info['formula']}`\n\n"
448
+
449
+ response += "**Parameters:**\n"
450
+ for param, value in params.items():
451
+ if isinstance(value, list):
452
+ value_str = f"[{', '.join(str(v) for v in value[:5])}" + ('...' if len(value) > 5 else ']')
453
+ elif param == 'r' and 'r' in params:
454
+ response += f"- {param} = {value:.1%}\n"
455
+ continue
456
+ else:
457
+ value_str = str(value)
458
+ response += f"- {param} = {value_str}\n"
459
+
460
+ response += f"\n**Result:** "
461
+
462
+ # Format result based on calculation type
463
+ result_value = result['value']
464
+ if calc_type in ['future_value', 'present_value', 'annuity_pv', 'annuity_fv', 'perpetuity_pv', 'npv']:
465
+ response += f"**${result_value:,.2f}**\n"
466
+ elif calc_type == 'irr':
467
+ response += f"**{result_value:.1%}**\n"
468
+ elif calc_type in ['correlation', 'covariance']:
469
+ response += f"**{result_value:.4f}**\n"
470
+ else:
471
+ response += f"**{result_value:.4f}**\n"
472
+
473
+ response += "\n**Calculation Steps:**\n"
474
+
475
+ # Add calculation steps based on type
476
+ if calc_type == 'future_value':
477
+ pv = params.get('PV', 0)
478
+ r = params.get('r', 0)
479
+ n = params.get('n', 0)
480
+ response += f"1. FV = PV × (1 + r)^n\n"
481
+ response += f"2. FV = ${pv:,.2f} × (1 + {r:.3f})^{n}\n"
482
+ response += f"3. FV = ${pv:,.2f} × {((1 + r) ** n):.4f}\n"
483
+ response += f"4. FV = **${result_value:,.2f}**\n"
484
+
485
+ elif calc_type == 'mean':
486
+ data = params.get('data', [])
487
+ response += f"1. Sum of values: {sum(data)}\n"
488
+ response += f"2. Number of values: {len(data)}\n"
489
+ response += f"3. Mean = {sum(data)} / {len(data)}\n"
490
+ response += f"4. Mean = **{result_value:.4f}**\n"
491
+
492
+ response += "\n💡 *Tip: Try varying the parameters to see how they affect the result.*"
493
+
494
+ return response
495
+
496
+ def _log_calculation(self, calc_type: str, params: Dict, result: Dict, user_profile: Dict):
497
+ """Log calculation for monitoring and analytics"""
498
+ log_entry = {
499
+ 'timestamp': datetime.now().isoformat(),
500
+ 'calculation_type': calc_type,
501
+ 'parameters': params,
502
+ 'result': result['value'],
503
+ 'user_level': user_profile.get('cfa_level', 'I'),
504
+ 'topics': user_profile.get('study_focus', [])
505
+ }
506
+ self.calculation_history.append(log_entry)
507
+
508
+ # Keep only last 1000 calculations
509
+ if len(self.calculation_history) > 1000:
510
+ self.calculation_history = self.calculation_history[-1000:]
511
+
512
+ def get_calculation_history(self, limit: int = 50) -> List[Dict]:
513
+ """Get recent calculation history"""
514
+ return self.calculation_history[-limit:]
515
+
516
+ def get_metrics(self) -> Dict[str, Any]:
517
+ """Get calculator performance metrics"""
518
+ if not self.calculation_history:
519
+ return {
520
+ 'total_calculations': 0,
521
+ 'most_common_type': None,
522
+ 'recent_accuracy': 1.0
523
+ }
524
+
525
+ # Count calculation types
526
+ type_counts = {}
527
+ for entry in self.calculation_history:
528
+ ctype = entry['calculation_type']
529
+ type_counts[ctype] = type_counts.get(ctype, 0) + 1
530
+
531
+ most_common = max(type_counts.items(), key=lambda x: x[1])[0] if type_counts else None
532
+
533
+ return {
534
+ 'total_calculations': len(self.calculation_history),
535
+ 'most_common_type': most_common,
536
+ 'type_distribution': type_counts,
537
+ 'recent_activity': len([e for e in self.calculation_history
538
+ if datetime.fromisoformat(e['timestamp']).date() == datetime.now().date()])
539
+ }