aip-agents-binary 0.6.1__py3-none-macosx_13_0_arm64.whl → 0.6.3__py3-none-macosx_13_0_arm64.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.
@@ -7,8 +7,6 @@ Required environment variables:
7
7
 
8
8
  import asyncio
9
9
 
10
- from langchain_openai import ChatOpenAI
11
-
12
10
  from aip_agents.agent import LangGraphReactAgent
13
11
  from aip_agents.ptc import PromptConfig, PTCSandboxConfig
14
12
 
@@ -24,7 +22,7 @@ async def main() -> None:
24
22
  agent = LangGraphReactAgent(
25
23
  name="ptc_hello_world",
26
24
  instruction=instruction,
27
- model=ChatOpenAI(model="gpt-5.2"),
25
+ model="openai/gpt-5.2",
28
26
  ptc_config=PTCSandboxConfig(enabled=True, sandbox_timeout=180.0, prompt=PromptConfig(mode="index")),
29
27
  )
30
28
  agent.add_mcp_server(
@@ -3,8 +3,12 @@
3
3
  from importlib import import_module
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from aip_agents.tools.date_range_tool import DateRangeTool
6
7
  from aip_agents.tools.gl_connector import GLConnectorTool
7
- from aip_agents.tools.gl_connector_tools import BOSA_AUTOMATED_TOOLS, GL_CONNECTORS_AUTOMATED_TOOLS
8
+ from aip_agents.tools.gl_connector_tools import (
9
+ BOSA_AUTOMATED_TOOLS,
10
+ GL_CONNECTORS_AUTOMATED_TOOLS,
11
+ )
8
12
  from aip_agents.tools.time_tool import TimeTool
9
13
  from aip_agents.tools.web_search import GoogleSerperTool
10
14
  from aip_agents.utils.logger import get_logger
@@ -17,6 +21,7 @@ __all__ = [
17
21
  "GLConnectorTool",
18
22
  "GoogleSerperTool",
19
23
  "TimeTool",
24
+ "DateRangeTool",
20
25
  ]
21
26
 
22
27
 
@@ -51,5 +56,9 @@ _register_optional("aip_agents.tools.execute_ptc_code", "create_execute_ptc_code
51
56
  if TYPE_CHECKING:
52
57
  from aip_agents.tools.browser_use import BrowserUseTool
53
58
  from aip_agents.tools.code_sandbox import E2BCodeSandboxTool
54
- from aip_agents.tools.document_loader import DocxReaderTool, ExcelReaderTool, PDFReaderTool
59
+ from aip_agents.tools.document_loader import (
60
+ DocxReaderTool,
61
+ ExcelReaderTool,
62
+ PDFReaderTool,
63
+ )
55
64
  from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool
@@ -1,5 +1,6 @@
1
1
  from aip_agents.tools.browser_use import BrowserUseTool as BrowserUseTool
2
2
  from aip_agents.tools.code_sandbox import E2BCodeSandboxTool as E2BCodeSandboxTool
3
+ from aip_agents.tools.date_range_tool import DateRangeTool as DateRangeTool
3
4
  from aip_agents.tools.document_loader import DocxReaderTool as DocxReaderTool, ExcelReaderTool as ExcelReaderTool, PDFReaderTool as PDFReaderTool
4
5
  from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool as create_execute_ptc_code_tool
5
6
  from aip_agents.tools.gl_connector import GLConnectorTool as GLConnectorTool
@@ -7,4 +8,4 @@ from aip_agents.tools.gl_connector_tools import BOSA_AUTOMATED_TOOLS as BOSA_AUT
7
8
  from aip_agents.tools.time_tool import TimeTool as TimeTool
8
9
  from aip_agents.tools.web_search import GoogleSerperTool as GoogleSerperTool
9
10
 
10
- __all__ = ['BOSA_AUTOMATED_TOOLS', 'GL_CONNECTORS_AUTOMATED_TOOLS', 'GLConnectorTool', 'GoogleSerperTool', 'TimeTool', 'BrowserUseTool', 'E2BCodeSandboxTool', 'DocxReaderTool', 'ExcelReaderTool', 'PDFReaderTool', 'create_execute_ptc_code_tool']
11
+ __all__ = ['BOSA_AUTOMATED_TOOLS', 'GL_CONNECTORS_AUTOMATED_TOOLS', 'GLConnectorTool', 'GoogleSerperTool', 'TimeTool', 'DateRangeTool', 'BrowserUseTool', 'E2BCodeSandboxTool', 'DocxReaderTool', 'ExcelReaderTool', 'PDFReaderTool', 'create_execute_ptc_code_tool']
@@ -0,0 +1,554 @@
1
+ """Tool to get date ranges for common time periods.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ Fachriza Dian Adhiatma (fachriza.d.adhiatma@gdplabs.id)
6
+ """
7
+
8
+ import json
9
+ import re
10
+ from collections.abc import Callable
11
+ from datetime import datetime, timedelta
12
+
13
+ from dateutil.relativedelta import relativedelta
14
+ from langchain_core.tools import BaseTool
15
+ from pydantic import BaseModel, Field
16
+
17
+ FORMAT_STRING = "%m/%d/%y %H:%M:%S"
18
+
19
+ MIN_DAYS_FOR_WEEK_SPLIT = 6 # Minimum days difference for week split (7 days inclusive = 6 days difference)
20
+ MONTH_NAME_ABBR_THRESHOLD = 3 # len > 3 implies full month name, else try abbreviated form
21
+ SUPPORTED_DATE_RANGES = [
22
+ "last_week",
23
+ "this_week",
24
+ "last_month",
25
+ "this_month",
26
+ "yesterday",
27
+ "today",
28
+ "last_7_days",
29
+ "last_30_days",
30
+ "this_quarter",
31
+ "last_quarter",
32
+ "this_year",
33
+ "last_year",
34
+ "[N]_days_ago",
35
+ "[N]_weeks_ago",
36
+ "[N]_months_ago",
37
+ "YYYY-MM-DD to YYYY-MM-DD",
38
+ ]
39
+
40
+
41
+ class DateRangeToolInput(BaseModel):
42
+ """Input schema for the DateRangeTool."""
43
+
44
+ date_range: str = Field(
45
+ ...,
46
+ description="""
47
+ Date range identifier. Supported values:
48
+ - Relative periods: 'last_week', 'this_week', 'last_month', 'this_month'
49
+ - Relative days: 'yesterday', 'today', 'last_7_days', 'last_30_days'
50
+ - Custom range: 'N_days_ago', 'N_weeks_ago', 'N_months_ago' (replace N with number)
51
+ - Quarter: 'this_quarter', 'last_quarter'
52
+ - Year: 'this_year', 'last_year'
53
+ - Natural forms: 'January 2025', 'January-March 2025', 'Q1 2025'
54
+ - Explicit date range: 'YYYY-MM-DD to YYYY-MM-DD' (e.g., '2025-08-01 to 2025-08-07')
55
+
56
+ Note: For queries like 'first week of August 2025', convert to explicit format: '2025-08-01 to 2025-08-07'
57
+ """,
58
+ )
59
+ format: str = Field(
60
+ default=FORMAT_STRING,
61
+ description="Optional datetime format string. Default: '%m/%d/%y'",
62
+ )
63
+ split_weeks: bool = Field(
64
+ default=False,
65
+ description=(
66
+ "If True and the resulting period spans more than one week, include a 'weeks' array of Sunday–Saturday"
67
+ " splits that fit within the requested period (not recent fixed weeks)."
68
+ ),
69
+ )
70
+
71
+
72
+ class DateRangeTool(BaseTool):
73
+ """Tool to get date ranges for common time periods."""
74
+
75
+ name: str = "date_range_tool"
76
+ description: str = """
77
+ Useful for getting date ranges for various time periods.
78
+ Supports relative dates, custom ranges, and standard periods.
79
+ Returns start and end dates for the specified period.
80
+ """
81
+ args_schema: type[BaseModel] = DateRangeToolInput
82
+
83
+ def _parse_custom_range(self, date_range: str) -> tuple[datetime, datetime]:
84
+ """Parse custom range patterns like '3_days_ago', '2_weeks_ago', etc.
85
+
86
+ Args:
87
+ date_range (str): A string representing the custom date range in the format '{number}_{unit}_ago',
88
+ where {number} is an integer and {unit} is one of 'days', 'weeks', or 'months'.
89
+
90
+ Returns:
91
+ tuple[datetime, datetime]: A tuple containing the start date and end date corresponding to the custom range.
92
+ """
93
+ pattern = r"(\d+)_(days|weeks|months)_ago"
94
+ match = re.match(pattern, date_range)
95
+ start_date = None
96
+ end_date = None
97
+ if not match:
98
+ raise ValueError(f"Invalid custom range format: {date_range}")
99
+
100
+ number, unit = int(match.group(1)), match.group(2)
101
+ today = datetime.now()
102
+
103
+ if unit == "days":
104
+ start_date = today - timedelta(days=number)
105
+ end_date = today
106
+ elif unit == "weeks":
107
+ start_date = today - timedelta(weeks=number)
108
+ end_date = today
109
+ elif unit == "months":
110
+ start_date = today - relativedelta(months=number)
111
+ end_date = today
112
+
113
+ return start_date, end_date
114
+
115
+ def _parse_month(self, month_str: str) -> int:
116
+ """Parse month name (full or abbreviated) to month number (1-12).
117
+
118
+ Args:
119
+ month_str (int): The month name string to parse.
120
+
121
+ Returns:
122
+ int: The month number (1-12).
123
+ """
124
+ fmt = "%B" if len(month_str) > MONTH_NAME_ABBR_THRESHOLD else "%b"
125
+ try:
126
+ return datetime.strptime(month_str, fmt).month
127
+ except ValueError:
128
+ # Fallback to abbreviated format
129
+ return datetime.strptime(month_str, "%b").month
130
+
131
+ def _parse_explicit_range(self, dr: str) -> tuple[datetime, datetime] | None:
132
+ """Parse explicit date range: YYYY-MM-DD to YYYY-MM-DD.
133
+
134
+ Args:
135
+ dr (str): The date range string to parse.
136
+
137
+ Returns:
138
+ tuple[datetime, datetime] | None: A tuple of (start_date, end_date) if matched, otherwise None.
139
+ """
140
+ m_explicit = re.match(r"^(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})$", dr, flags=re.IGNORECASE)
141
+ if m_explicit:
142
+ try:
143
+ start = datetime.strptime(m_explicit.group(1), "%Y-%m-%d")
144
+ end = datetime.strptime(m_explicit.group(2), "%Y-%m-%d")
145
+ if start > end:
146
+ raise ValueError("Start date must be before or equal to end date")
147
+ return start, end
148
+ except ValueError:
149
+ return None
150
+ return None
151
+
152
+ def _parse_quarter_with_year(self, dr: str) -> tuple[datetime, datetime] | None:
153
+ """Parse quarter with year: Q1 2025.
154
+
155
+ Args:
156
+ dr (str): The date range string to parse.
157
+
158
+ Returns:
159
+ tuple[datetime, datetime] | None: A tuple of (start_date, end_date) if matched, otherwise None.
160
+ """
161
+ m_q = re.match(r"^Q([1-4])\s+(\d{4})$", dr, flags=re.IGNORECASE)
162
+ if m_q:
163
+ q = int(m_q.group(1))
164
+ year = int(m_q.group(2))
165
+ start_month = (q - 1) * 3 + 1
166
+ start = datetime(year, start_month, 1)
167
+ end = start + relativedelta(months=3, days=-1)
168
+ return start, end
169
+ return None
170
+
171
+ def _parse_month_range_with_year(self, dr: str) -> tuple[datetime, datetime] | None:
172
+ """Parse month range within a year: January-March 2025.
173
+
174
+ Args:
175
+ dr (str): The date range string to parse.
176
+
177
+ Returns:
178
+ tuple[datetime, datetime] | None: A tuple of (start_date, end_date) if matched, otherwise None.
179
+ """
180
+ m_range = re.match(r"^([A-Za-z]{3,9})\s*-\s*([A-Za-z]{3,9})\s+(\d{4})$", dr)
181
+ if m_range:
182
+ m1, m2, year_s = m_range.groups()
183
+ year = int(year_s)
184
+ try:
185
+ start_month = self._parse_month(m1)
186
+ end_month = self._parse_month(m2)
187
+ start = datetime(year, start_month, 1)
188
+ end = datetime(year, end_month, 1) + relativedelta(months=1, days=-1)
189
+ return start, end
190
+ except ValueError:
191
+ return None
192
+ return None
193
+
194
+ def _parse_single_month_with_year(self, dr: str) -> tuple[datetime, datetime] | None:
195
+ """Parse single month with year: January 2025.
196
+
197
+ Args:
198
+ dr (str): The date range string to parse.
199
+
200
+ Returns:
201
+ tuple[datetime, datetime] | None: A tuple of (start_date, end_date) if matched, otherwise None.
202
+ """
203
+ m_single = re.match(r"^([A-Za-z]{3,9})\s+(\d{4})$", dr)
204
+ if m_single:
205
+ mname, year_s = m_single.groups()
206
+ year = int(year_s)
207
+ try:
208
+ month = self._parse_month(mname)
209
+ start = datetime(year, month, 1)
210
+ end = start + relativedelta(months=1, days=-1)
211
+ return start, end
212
+ except ValueError:
213
+ return None
214
+ return None
215
+
216
+ def _parse_month_or_range(self, date_range: str) -> tuple[datetime, datetime] | None:
217
+ """Parse inputs like 'January 2025', 'Jan 2025', 'January-March 2025', 'Jan-Mar 2025', 'Q1 2025'.
218
+
219
+ Also parses explicit date ranges like '2025-08-01 to 2025-08-07'.
220
+
221
+ Args:
222
+ date_range (str): The date range string to parse.
223
+ Can be natural month/quarter forms or explicit YYYY-MM-DD format.
224
+
225
+ Returns:
226
+ tuple[datetime, datetime] | None: A tuple of (start_date, end_date) if matched, otherwise None.
227
+
228
+ Raises:
229
+ ValueError: If start date is after end date (caught internally and returns None).
230
+
231
+ Examples:
232
+ >>> self._parse_month_or_range("January 2025")
233
+ (datetime(2025, 1, 1), datetime(2025, 1, 31))
234
+ >>> self._parse_month_or_range("2025-08-01 to 2025-08-07")
235
+ (datetime(2025, 8, 1), datetime(2025, 8, 7))
236
+ """
237
+ dr = date_range.strip()
238
+
239
+ # Try each parser in order
240
+ parsers = [
241
+ self._parse_explicit_range,
242
+ self._parse_quarter_with_year,
243
+ self._parse_month_range_with_year,
244
+ self._parse_single_month_with_year,
245
+ ]
246
+
247
+ for parser in parsers:
248
+ result = parser(dr)
249
+ if result:
250
+ return result
251
+
252
+ return None
253
+
254
+ def _get_quarter_dates(self, today: datetime, last_quarter: bool = False) -> tuple[datetime, datetime]:
255
+ """Calculate the start and end dates of the current or last quarter based on the given date.
256
+
257
+ Args:
258
+ today (datetime): The reference date to determine the quarter.
259
+ last_quarter (bool, optional): If True, calculate the dates for the last quarter.
260
+ If False, calculate the dates for the current quarter.
261
+ Defaults to False.
262
+
263
+ Returns:
264
+ tuple[datetime, datetime]: A tuple containing the start and end dates of the quarter.
265
+ """
266
+ current_quarter = (today.month - 1) // 3
267
+ if last_quarter:
268
+ if current_quarter == 0:
269
+ start_date = datetime(today.year - 1, 10, 1)
270
+ end_date = datetime(today.year - 1, 12, 31)
271
+ else:
272
+ start_date = datetime(today.year, 3 * (current_quarter - 1) + 1, 1)
273
+ end_date = datetime(today.year, 3 * current_quarter, 1) + relativedelta(months=1, days=-1)
274
+ else:
275
+ start_date = datetime(today.year, 3 * current_quarter + 1, 1)
276
+ end_date = datetime(today.year, 3 * (current_quarter + 1), 1) + relativedelta(months=1, days=-1)
277
+ return start_date, end_date
278
+
279
+ def _days_since_most_recent_sunday(self, date: datetime) -> int:
280
+ """Return days since most recent Sunday (0 if today is Sunday).
281
+
282
+ Args:
283
+ date (datetime): The reference date.
284
+
285
+ Returns:
286
+ int: Days since most recent Sunday (0-6).
287
+ """
288
+ # weekday(): Mon=0, Tue=1, ..., Sun=6
289
+ # We want Sun=0, Mon=1, ..., Sat=6
290
+ return (date.weekday() + 1) % 7
291
+
292
+ def _get_standard_period_dates(self, date_range: str, today: datetime) -> tuple[datetime, datetime]:
293
+ """Calculate the start and end dates for a given standard period relative to a specified date.
294
+
295
+ Args:
296
+ date_range (str): The standard period to calculate dates for.
297
+ Supported values are "last_week", "this_week", "last_month", and "this_month".
298
+ today (datetime): The reference date to calculate the period from.
299
+
300
+ Returns:
301
+ tuple[datetime, datetime]: A tuple containing the start and end dates of the specified period.
302
+ """
303
+ if date_range == "last_week":
304
+ # Calculate the previous Saturday (end of last week)
305
+ days_since_sunday = self._days_since_most_recent_sunday(today)
306
+ last_saturday = today - timedelta(days=days_since_sunday + 1)
307
+
308
+ # Calculate the previous Sunday (start of last week)
309
+ last_sunday = last_saturday - timedelta(days=6)
310
+
311
+ return last_sunday, last_saturday
312
+
313
+ if date_range == "this_week":
314
+ # Calculate the most recent Sunday (start of this week)
315
+ days_since_sunday = self._days_since_most_recent_sunday(today)
316
+ sunday = today - timedelta(days=days_since_sunday)
317
+
318
+ # Calculate the upcoming Saturday (end of this week)
319
+ saturday = sunday + timedelta(days=6)
320
+
321
+ return sunday, saturday
322
+
323
+ if date_range == "last_month":
324
+ first_day = today.replace(day=1)
325
+ last_month = first_day - timedelta(days=1)
326
+ return last_month.replace(day=1), last_month
327
+
328
+ if date_range == "this_month":
329
+ start = today.replace(day=1)
330
+ return start, start + relativedelta(months=1, days=-1)
331
+
332
+ raise ValueError(f"Unknown standard period: {date_range}")
333
+
334
+ def _week_bounds_for(self, date: datetime) -> tuple[datetime, datetime]:
335
+ """Return the Sunday-to-Saturday week containing the given date.
336
+
337
+ - Sunday is the start of week.
338
+ - Saturday is the end of week.
339
+
340
+ Args:
341
+ date (datetime): The date to find the week bounds for.
342
+
343
+ Returns:
344
+ tuple[datetime, datetime]: A tuple containing the start and end dates of the week.
345
+ """
346
+ days_since_sunday = self._days_since_most_recent_sunday(date)
347
+ sunday = datetime(date.year, date.month, date.day) - timedelta(days=days_since_sunday)
348
+ saturday = sunday + timedelta(days=6)
349
+ return sunday, saturday
350
+
351
+ def _build_week_splits(self, start_date: datetime, end_date: datetime, fmt: str) -> list[dict[str, str]]:
352
+ """Build Sunday–Saturday week splits that intersect [start_date, end_date].
353
+
354
+ - Includes partial edge weeks if any day of the Sun–Sat week falls within the requested range.
355
+ - Week entries still use full Sunday–Saturday bounds for consistency.
356
+ - Labels weeks consecutively as week_1, week_2, ...
357
+
358
+ Args:
359
+ start_date (datetime): Start of requested period.
360
+ end_date (datetime): End of requested period.
361
+ fmt (str): Format string for dates.
362
+
363
+ Returns:
364
+ list[dict]: Weekly split entries with start_date, end_date, and human description.
365
+ """
366
+ weeks: list[dict] = []
367
+ # Anchor on the Sunday of the week that contains start_date
368
+ first_week_sunday, _ = self._week_bounds_for(start_date)
369
+ cursor = first_week_sunday
370
+ idx = 1
371
+ while cursor <= end_date:
372
+ full_week_start = cursor
373
+ full_week_end = cursor + timedelta(days=6)
374
+ # Include any week that intersects the requested range
375
+ if not (full_week_end < start_date or full_week_start > end_date):
376
+ desc = f"From {full_week_start.strftime('%B %d, %Y')} to {full_week_end.strftime('%B %d, %Y')}"
377
+ weeks.append(
378
+ {
379
+ "period": f"week_{idx}",
380
+ "start_date": full_week_start.strftime(fmt),
381
+ "end_date": full_week_end.strftime(fmt),
382
+ "description": desc,
383
+ }
384
+ )
385
+ idx += 1
386
+ cursor += timedelta(days=7)
387
+ return weeks
388
+
389
+ def _get_relative_day_dates(self, date_range: str, today: datetime) -> tuple[datetime, datetime]:
390
+ """Calculate the start and end dates for a given relative date range.
391
+
392
+ Args:
393
+ date_range (str): The relative date range. Supported values are "yesterday", "today",
394
+ "last_7_days", and "last_30_days".
395
+ today (datetime): The reference date from which the relative date range is calculated.
396
+
397
+ Returns:
398
+ tuple[datetime, datetime]: A tuple containing the start and end dates for the specified
399
+ relative date range.
400
+ """
401
+ if date_range == "yesterday":
402
+ yesterday = today - timedelta(days=1)
403
+ return yesterday, yesterday
404
+
405
+ if date_range == "today":
406
+ return today, today
407
+
408
+ if date_range == "last_7_days":
409
+ return today - timedelta(days=7), today
410
+
411
+ if date_range == "last_30_days":
412
+ return today - timedelta(days=30), today
413
+
414
+ raise ValueError(f"Unknown relative day range: {date_range}")
415
+
416
+ def _get_year_dates(self, date_range: str, today: datetime) -> tuple[datetime, datetime]:
417
+ """Returns the start and end dates for the specified year range.
418
+
419
+ Args:
420
+ date_range (str): The year range to calculate dates for.
421
+ Accepted values are "this_year" and "last_year".
422
+ today (datetime): The current date to base the year calculation on.
423
+
424
+ Returns:
425
+ Tuple[datetime, datetime]: A tuple containing the start and end dates for the specified year range.
426
+ """
427
+ if date_range == "this_year":
428
+ return datetime(today.year, 1, 1), datetime(today.year, 12, 31)
429
+
430
+ if date_range == "last_year":
431
+ return datetime(today.year - 1, 1, 1), datetime(today.year - 1, 12, 31)
432
+
433
+ raise ValueError(f"Unknown year range: {date_range}")
434
+
435
+ def _format_response(
436
+ self,
437
+ start_date: datetime,
438
+ end_date: datetime,
439
+ date_range: str,
440
+ format: str,
441
+ weeks: list[dict] | None = None,
442
+ ) -> str:
443
+ """Format the response as a JSON string with the given date range information.
444
+
445
+ Args:
446
+ start_date (datetime): The start date of the range.
447
+ end_date (datetime): The end date of the range.
448
+ date_range (str): A string representation of the date range.
449
+ format (str): The format string to use for formatting the dates.
450
+ weeks (list[dict] | None): Optional weekly splits (Sunday–Saturday) for the period. This
451
+ is included when `split_weeks` is True and the overall period spans more than one week.
452
+
453
+ Returns:
454
+ str: A JSON string containing the formatted start date, end date, period, and description.
455
+ """
456
+ if weeks:
457
+ payload: dict = {
458
+ "weeks": weeks,
459
+ "period": date_range,
460
+ "description": f"Split into {len(weeks)} weeks with Sunday-to-Saturday bounds",
461
+ }
462
+ return json.dumps(payload)
463
+ payload: dict = {
464
+ "start_date": start_date.strftime(format),
465
+ "end_date": end_date.strftime(format),
466
+ "period": date_range,
467
+ "description": f"From {start_date.strftime('%B %d, %Y')} to {end_date.strftime('%B %d, %Y')}",
468
+ }
469
+ return json.dumps(payload)
470
+
471
+ def _get_result_and_format(
472
+ self,
473
+ date_range: str,
474
+ format: str,
475
+ split_weeks: bool,
476
+ result: tuple[datetime, datetime],
477
+ ) -> str:
478
+ """Helper to format response with optional week splits.
479
+
480
+ Args:
481
+ date_range (str): The date range string to process.
482
+ format (str): The format string to use for the output.
483
+ split_weeks (bool): If True and the resulting period spans more than one week,
484
+ also include a 'weeks' array with Sunday-to-Saturday splits for the recent weeks.
485
+ result (tuple[datetime, datetime]): A tuple containing the start and end dates of the date range.
486
+
487
+ Returns:
488
+ str: A formatted date range string or an error message if the date range is unsupported or an error occurs.
489
+ """
490
+ start_date, end_date = result
491
+ weeks = None
492
+ # Only build week splits when requested and range spans more than one week
493
+ if split_weeks and (end_date - start_date).days >= MIN_DAYS_FOR_WEEK_SPLIT:
494
+ weeks = self._build_week_splits(start_date, end_date, format)
495
+ return self._format_response(start_date, end_date, date_range, format, weeks=weeks)
496
+
497
+ def _run(self, date_range: str, format: str = FORMAT_STRING, split_weeks: bool = False) -> str:
498
+ """Process a given date range string and return a formatted date range string.
499
+
500
+ Args:
501
+ date_range (str): The date range string to process. Supported formats include:
502
+ - "standard_periods"
503
+ - "relative_days"
504
+ - "quarters"
505
+ - "years"
506
+ - "custom" (e.g., "10_days_ago", "2_weeks_ago", "3_months_ago")
507
+ format (str, optional): The format string to use for the output. Defaults to FORMAT_STRING.
508
+ split_weeks (bool, optional): If True and the resulting period spans more than one week,
509
+ also include a 'weeks' array with Sunday-to-Saturday splits for the recent weeks.
510
+
511
+ Returns:
512
+ str: A formatted date range string or an error message if the date range is unsupported or an error occurs.
513
+ """
514
+ today = datetime.now()
515
+
516
+ try:
517
+ handlers: dict[str, Callable] = {
518
+ "standard_periods": lambda: self._get_standard_period_dates(date_range, today),
519
+ "relative_days": lambda: self._get_relative_day_dates(date_range, today),
520
+ # Only handle special tokens 'this_quarter' and 'last_quarter' here. Natural forms like 'Q1 2025'
521
+ # are parsed later by _parse_month_or_range(). For non-matching inputs, return None so we keep trying.
522
+ "quarters": lambda: (
523
+ self._get_quarter_dates(today, last_quarter=(date_range == "last_quarter"))
524
+ if date_range in {"this_quarter", "last_quarter"}
525
+ else None
526
+ ),
527
+ "years": lambda: self._get_year_dates(date_range, today),
528
+ "custom": lambda: (
529
+ self._parse_custom_range(date_range)
530
+ if re.match(r"\d+_(days|weeks|months)_ago", date_range)
531
+ else None
532
+ ),
533
+ }
534
+
535
+ for handler in handlers.values():
536
+ try:
537
+ result = handler()
538
+ if result:
539
+ return self._get_result_and_format(date_range, format, split_weeks, result)
540
+ except ValueError:
541
+ continue
542
+
543
+ # Try parsing natural month/quarter expressions
544
+ mr = self._parse_month_or_range(date_range)
545
+ if mr:
546
+ return self._get_result_and_format(date_range, format, split_weeks, mr)
547
+
548
+ return (
549
+ f"Unsupported date range: {date_range}, supported date ranges are: {SUPPORTED_DATE_RANGES} "
550
+ f"or natural month/quarter forms like 'January 2025', 'January-March 2025', 'Q1 2025'."
551
+ )
552
+
553
+ except Exception as e:
554
+ return f"Error processing date range: {str(e)}"
@@ -0,0 +1,21 @@
1
+ from _typeshed import Incomplete
2
+ from collections.abc import Callable as Callable
3
+ from langchain_core.tools import BaseTool
4
+ from pydantic import BaseModel
5
+
6
+ FORMAT_STRING: str
7
+ MIN_DAYS_FOR_WEEK_SPLIT: int
8
+ MONTH_NAME_ABBR_THRESHOLD: int
9
+ SUPPORTED_DATE_RANGES: Incomplete
10
+
11
+ class DateRangeToolInput(BaseModel):
12
+ """Input schema for the DateRangeTool."""
13
+ date_range: str
14
+ format: str
15
+ split_weeks: bool
16
+
17
+ class DateRangeTool(BaseTool):
18
+ """Tool to get date ranges for common time periods."""
19
+ name: str
20
+ description: str
21
+ args_schema: type[BaseModel]
@@ -18,6 +18,7 @@ from langchain_core.callbacks import (
18
18
  CallbackManagerForToolRun,
19
19
  )
20
20
  from langchain_core.tools import BaseTool
21
+ from pydantic import BaseModel, Field
21
22
 
22
23
  from aip_agents.ptc.naming import sanitize_function_name
23
24
  from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY
@@ -31,6 +32,21 @@ if TYPE_CHECKING:
31
32
  logger = get_logger(__name__)
32
33
 
33
34
 
35
+ class PTCCodeInput(BaseModel):
36
+ """Input schema for PTCCodeTool."""
37
+
38
+ code: str = Field(
39
+ ...,
40
+ description=(
41
+ "Python code to execute. Import MCP tools from the generated `tools` package, "
42
+ "for example: `from tools.yfinance import get_stock_history`. "
43
+ "The code runs in a sandboxed environment with access to all configured MCP tools. "
44
+ "Use print() to output results. The tool returns JSON with keys: "
45
+ "ok, stdout, stderr, exit_code."
46
+ ),
47
+ )
48
+
49
+
34
50
  def _merge_config_layer(
35
51
  merged: dict[str, dict[str, Any]],
36
52
  source: dict[str, Any],
@@ -123,6 +139,9 @@ class PTCCodeTool(BaseTool):
123
139
  "This tool is useful for chaining multiple MCP tool calls with local data processing."
124
140
  )
125
141
 
142
+ # Input schema for LangChain tool invocation
143
+ args_schema: type[BaseModel] = PTCCodeInput
144
+
126
145
  # Internal attributes (not exposed to LLM)
127
146
  _ptc_executor: "PTCSandboxExecutor" = None # type: ignore[assignment]
128
147
  _ptc_runtime: "E2BSandboxRuntime" = None # type: ignore[assignment]
@@ -149,22 +168,6 @@ class PTCCodeTool(BaseTool):
149
168
  object.__setattr__(self, "_ptc_runtime", runtime)
150
169
  object.__setattr__(self, "_agent_tool_configs", agent_tool_configs)
151
170
 
152
- @property
153
- def args(self) -> dict[str, Any]:
154
- """Return the argument schema for the tool."""
155
- return {
156
- "code": {
157
- "type": "string",
158
- "description": (
159
- "Python code to execute. Import MCP tools from the generated `tools` package, "
160
- "for example: `from tools.yfinance import get_stock_history`. "
161
- "The code runs in a sandboxed environment with access to all configured MCP tools. "
162
- "Use print() to output results. The tool returns JSON with keys: "
163
- "ok, stdout, stderr, exit_code."
164
- ),
165
- }
166
- }
167
-
168
171
  def _run(
169
172
  self,
170
173
  code: str,
@@ -6,10 +6,15 @@ from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime as E2BSandboxRuntim
6
6
  from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY as TOOL_CONFIGS_KEY
7
7
  from aip_agents.utils.logger import get_logger as get_logger
8
8
  from langchain_core.tools import BaseTool
9
+ from pydantic import BaseModel
9
10
  from typing import Any
10
11
 
11
12
  logger: Incomplete
12
13
 
14
+ class PTCCodeInput(BaseModel):
15
+ """Input schema for PTCCodeTool."""
16
+ code: str
17
+
13
18
  def merge_tool_configs(agent_configs: dict[str, Any] | None, runtime_configs: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
14
19
  '''Merge agent-level and runtime tool configs with sanitized keys.
15
20
 
@@ -42,6 +47,7 @@ class PTCCodeTool(BaseTool):
42
47
  """
43
48
  name: str
44
49
  description: str
50
+ args_schema: type[BaseModel]
45
51
  def __init__(self, executor: PTCSandboxExecutor, runtime: E2BSandboxRuntime, agent_tool_configs: dict[str, Any] | None = None, **kwargs: Any) -> None:
46
52
  """Initialize the PTC code tool.
47
53
 
@@ -51,9 +57,6 @@ class PTCCodeTool(BaseTool):
51
57
  agent_tool_configs: Optional agent-level tool configs.
52
58
  **kwargs: Additional keyword arguments passed to BaseTool.
53
59
  """
54
- @property
55
- def args(self) -> dict[str, Any]:
56
- """Return the argument schema for the tool."""
57
60
  async def cleanup(self) -> None:
58
61
  """Clean up the sandbox runtime."""
59
62
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aip-agents-binary
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: A library for managing agents in Gen AI applications.
5
5
  Author-email: Raymond Christopher <raymond.christopher@gdplabs.id>
6
6
  Requires-Python: <3.13,>=3.11
@@ -24,6 +24,7 @@ Requires-Dist: langchain-experimental<0.4.0,>=0.3.4
24
24
  Requires-Dist: langgraph<0.7.0,>=0.6.0
25
25
  Requires-Dist: minio<8.0.0,>=7.2.20
26
26
  Requires-Dist: pydantic<3.0.0,>=2.11.7
27
+ Requires-Dist: python-dateutil<3.0.0,>=2.9.0
27
28
  Requires-Dist: python-dotenv<2.0.0,>=1.1.0
28
29
  Requires-Dist: requests<3.0.0,>=2.32.4
29
30
  Requires-Dist: uvicorn<0.35.0,>=0.34.3
@@ -209,7 +209,7 @@ aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py,sha256=sppLD
209
209
  aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.pyi,sha256=zFNns9i-sPY8aBy4HaVS6tXDWTsU7f-ApLD8gDtb1uk,252
210
210
  aip_agents/examples/hello_world_pii_logger.py,sha256=pm9LK-doZwQL-VdJYFHziuvwH562c8wwAwmKZeAWQS0,584
211
211
  aip_agents/examples/hello_world_pii_logger.pyi,sha256=5gOVrvhQV2DxIwr7ocL1-oTUXsO8XSyl5WN70c7rl_w,134
212
- aip_agents/examples/hello_world_ptc.py,sha256=EkSHUfRiGWLhUBcwpjWTRzAILqYzEPpys0pXmndWjk0,1462
212
+ aip_agents/examples/hello_world_ptc.py,sha256=A3aadYO3H41UgKZoHqrThXz2_hOFGX8-E4XwFnRi7P4,1410
213
213
  aip_agents/examples/hello_world_ptc.pyi,sha256=Dbo4myHbndrL6PyA_mgf3HTIYqLudTaF4h_nsdqOyKs,231
214
214
  aip_agents/examples/hello_world_sentry.py,sha256=Gccr6WCevSnMFZYT5M6uJ_22EvLmAMUE8Et_H510_kM,3976
215
215
  aip_agents/examples/hello_world_sentry.pyi,sha256=k0wFj46QqXEjBU0CEERmiEf2bovVFkupzuzQzeD6h00,671
@@ -434,12 +434,14 @@ aip_agents/storage/providers/memory.py,sha256=81E9yT_oRkH7tMVr5azZv1oXIaeTzTAtTu
434
434
  aip_agents/storage/providers/memory.pyi,sha256=r0meeCsjKyE9FHJqCysfQ5ZH_FWRRv2SWg-NOHCUD-g,2109
435
435
  aip_agents/storage/providers/object_storage.py,sha256=Q268Sn8Y09gS-ilP6fe4CR0YIOvdbunh3V6SmuQsAVs,6413
436
436
  aip_agents/storage/providers/object_storage.pyi,sha256=nBIKJjUAWkIKhYhBFmFcypjoOieIYOghy55f62x7AX8,2878
437
- aip_agents/tools/__init__.py,sha256=Kvd185p8zZ2xpNqimxURJUA6joPXP-3MIrr8K3Lf9Rw,2102
438
- aip_agents/tools/__init__.pyi,sha256=K2j6MjKo5aaMAbWkknLwyRbSu14LxJyRMoi88DbHROk,1027
437
+ aip_agents/tools/__init__.py,sha256=ullhWvRVtKYAEO5XhXn1sc1dT1LYDViSc23uqywUwyg,2228
438
+ aip_agents/tools/__init__.pyi,sha256=KmdVTX-WXRChSqCyGrAUoV8TwpUBXlo9ITgx4xpqYx8,1120
439
439
  aip_agents/tools/constants.py,sha256=AabnuPQG_mc2sVdr9jV23_6bFempAsxQv3kdCR_ztLA,5723
440
440
  aip_agents/tools/constants.pyi,sha256=kFY8dKgRqHKGvPqG3QUyuEc8C5yRaDj7DYQDH88K2T0,3552
441
- aip_agents/tools/execute_ptc_code.py,sha256=h1-bnW4u5HoDFZGQbROitREE4MGblvlwKqHXSNL3XUM,11408
442
- aip_agents/tools/execute_ptc_code.pyi,sha256=4wXrAPjUYoXWODxg90expdvIetTlULvFZwGvKbdbJrE,4046
441
+ aip_agents/tools/date_range_tool.py,sha256=t0vQqb0du1Q2n4q8V_-TPmfjSuwr4MYRMi6OpXMvVrw,22807
442
+ aip_agents/tools/date_range_tool.pyi,sha256=1Caen334PHliSJOmXpu94kIFP_PPOdOj3dhKqdfMe8o,570
443
+ aip_agents/tools/execute_ptc_code.py,sha256=mN8-G50voxVqWH9r3uJGQOtgPtxsHaTOnJrJuojQLIM,11396
444
+ aip_agents/tools/execute_ptc_code.pyi,sha256=nDlpxV-kcKuNmtghahjXAtjWvtNv6D38x8-VTNCKYjU,4089
443
445
  aip_agents/tools/gl_connector_tools.py,sha256=bxl_3VQYZDv3lFn6Y3kDVVRFwH4cntOLz3f74YzDcic,3936
444
446
  aip_agents/tools/gl_connector_tools.pyi,sha256=2ATn_MW_FRg5Uv7dLI_ToBOtlgTSfj0zgDQpN1N-cJs,1366
445
447
  aip_agents/tools/memory_search_tool.py,sha256=gqTTb_Fao_Hl-SavfaFsqPDhnFQUjzIQUkzizSD2L0A,653
@@ -606,7 +608,7 @@ aip_agents/utils/pii/pii_helper.py,sha256=g0yRzakfA2AA6vjUNLWHqFlcxyLql6MXQ90NN3
606
608
  aip_agents/utils/pii/pii_helper.pyi,sha256=dulZs150ikbAL3Bw2YLcz3_g4DsGmL3lciwf8mKxEjI,2939
607
609
  aip_agents/utils/pii/uuid_deanonymizer_mapping.py,sha256=Gks8l8t0cuS9pzoQnrpiK1CaLmWYksjOnTeiHh3_7EE,7348
608
610
  aip_agents/utils/pii/uuid_deanonymizer_mapping.pyi,sha256=gnWfD1rWZh_tloJjgKiZ6f6iNUuBaHpKqCSiP0d-9bs,3084
609
- aip_agents_binary-0.6.1.dist-info/METADATA,sha256=SHkg6ELXvsKcMmpLPVaJSxPYoSHbpMqUmhouUUFqAVg,22371
610
- aip_agents_binary-0.6.1.dist-info/WHEEL,sha256=KxCTaSkoYs_EnWvWxmau4HAvN-_rCRYV_bfRc_41A9k,106
611
- aip_agents_binary-0.6.1.dist-info/top_level.txt,sha256=PEz8vcwC1bH4UrkhF0LkIYCNfXGWZUHdSklbvkBe25E,11
612
- aip_agents_binary-0.6.1.dist-info/RECORD,,
611
+ aip_agents_binary-0.6.3.dist-info/METADATA,sha256=7BAliBsUy5k6VWI4QIMhyqul_9tjgYDM4NtsxIr64ak,22416
612
+ aip_agents_binary-0.6.3.dist-info/WHEEL,sha256=KxCTaSkoYs_EnWvWxmau4HAvN-_rCRYV_bfRc_41A9k,106
613
+ aip_agents_binary-0.6.3.dist-info/top_level.txt,sha256=PEz8vcwC1bH4UrkhF0LkIYCNfXGWZUHdSklbvkBe25E,11
614
+ aip_agents_binary-0.6.3.dist-info/RECORD,,