langchain-lexio 0.1.0__tar.gz
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.
- langchain_lexio-0.1.0/.gitignore +6 -0
- langchain_lexio-0.1.0/CHANGELOG.md +5 -0
- langchain_lexio-0.1.0/LICENSE +21 -0
- langchain_lexio-0.1.0/PKG-INFO +111 -0
- langchain_lexio-0.1.0/README.md +82 -0
- langchain_lexio-0.1.0/langchain_lexio/__init__.py +24 -0
- langchain_lexio-0.1.0/langchain_lexio/py.typed +0 -0
- langchain_lexio-0.1.0/langchain_lexio/toolkit.py +61 -0
- langchain_lexio-0.1.0/langchain_lexio/tools.py +317 -0
- langchain_lexio-0.1.0/pyproject.toml +42 -0
- langchain_lexio-0.1.0/tests/integration_tests/test_live.py +52 -0
- langchain_lexio-0.1.0/tests/unit_tests/test_tools.py +153 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lexio Rent (https://lexio.rent)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-lexio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools for the Lexio Rent API (car rental software): fleet, availability and prices, rentals, customers and quotes.
|
|
5
|
+
Project-URL: Homepage, https://lexio.rent/logiciel-location-voiture
|
|
6
|
+
Project-URL: Documentation, https://github.com/lexio-rent-git/langchain-lexio#readme
|
|
7
|
+
Project-URL: API reference, https://docs.lexio.rent
|
|
8
|
+
Project-URL: Source, https://github.com/lexio-rent-git/langchain-lexio
|
|
9
|
+
Project-URL: Issues, https://github.com/lexio-rent-git/langchain-lexio/issues
|
|
10
|
+
Author-email: Lexio Rent <contact@lexio.rent>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: agents,car-rental,fleet-management,langchain,langchain-tools,lexio,lexio-rent
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Office/Business
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: langchain-core<2,>=0.3
|
|
22
|
+
Requires-Dist: lexio-rent>=0.1.1
|
|
23
|
+
Requires-Dist: pydantic>=2
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: langchain-tests>=0.3; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# langchain-lexio
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/langchain-lexio/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
LangChain tools for the [Lexio Rent API](https://docs.lexio.rent). They let an agent answer questions about a vehicle rental company: which vehicles are free next weekend and at what price, which bookings start tomorrow, what a customer has rented, and (optionally) prepare a priced quote.
|
|
36
|
+
|
|
37
|
+
<a href="https://lexio.rent/logiciel-location-voiture">Lexio Rent</a> is rental management software for professional car, van and two-wheeler rental companies: planning, contracts, check-in/check-out inspections, deposits and fleet maintenance.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install -U langchain-lexio
|
|
43
|
+
export LEXIO_API_KEY="lck_..."
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The API key is created in Lexio under **Settings → Developer → API tokens**. API access is part of the Business plan. There is no sandbox: test against a dedicated organization.
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
| Tool | Name | What it does | Writes? |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `LexioSearchVehiclesTool` | `lexio_search_vehicles` | Active fleet with plate, model, type, status and odometer | no |
|
|
53
|
+
| `LexioCheckAvailabilityTool` | `lexio_check_availability` | Bookable vehicles for a period, with the total price | no |
|
|
54
|
+
| `LexioListRentalsTool` | `lexio_list_rentals` | Rentals by status, date window, vehicle or customer | no |
|
|
55
|
+
| `LexioGetRentalTool` | `lexio_get_rental` | One rental with all its details | no |
|
|
56
|
+
| `LexioFindCustomerTool` | `lexio_find_customer` | Customers by email or company registration number | no |
|
|
57
|
+
| `LexioListCategoriesTool` | `lexio_list_categories` | Vehicle categories (pricing classes) | no |
|
|
58
|
+
| `LexioCreateQuoteTool` | `lexio_create_quote` | Priced quote for a vehicle and a period; books nothing | quote only |
|
|
59
|
+
|
|
60
|
+
`LexioToolkit` returns the read-only tools. Pass `allow_quotes=True` to add `lexio_create_quote`. No tool creates, changes or cancels a rental, a payment or a customer record.
|
|
61
|
+
|
|
62
|
+
Dates are the rental company's local time without offset (`2026-12-14T09:00:00`). Lists are capped by `max_results` (default 20) so that tool outputs stay small.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from langchain.agents import create_agent
|
|
68
|
+
from langchain_lexio import LexioToolkit
|
|
69
|
+
|
|
70
|
+
tools = LexioToolkit(allow_quotes=True).get_tools()
|
|
71
|
+
|
|
72
|
+
agent = create_agent(
|
|
73
|
+
"anthropic:claude-sonnet-5-5",
|
|
74
|
+
tools=tools,
|
|
75
|
+
system_prompt="You help the counter staff of a car rental agency. Dates are local time.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
result = agent.invoke(
|
|
79
|
+
{"messages": [{"role": "user", "content": "Which vans are free from 14 to 16 December, and how much?"}]}
|
|
80
|
+
)
|
|
81
|
+
print(result["messages"][-1].content)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A single tool can also be called directly:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from langchain_lexio import LexioCheckAvailabilityTool
|
|
88
|
+
|
|
89
|
+
tool = LexioCheckAvailabilityTool()
|
|
90
|
+
print(tool.invoke({"start_at": "2026-12-14T09:00:00", "end_at": "2026-12-16T18:00:00", "vehicle_type": "VAN"}))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
API errors are raised as typed exceptions from the [`lexio-rent`](https://pypi.org/project/lexio-rent/) client (`NotFoundError`, `ValidationError`, `RateLimitError`...), with the Lexio error code and request id.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install -e ".[test]"
|
|
99
|
+
pytest tests/unit_tests # offline, includes the LangChain standard unit tests
|
|
100
|
+
LEXIO_API_KEY=lck_... pytest tests/integration_tests # LangChain standard integration tests, live
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## About
|
|
104
|
+
|
|
105
|
+
`langchain-lexio` is maintained by the team behind <a href="https://lexio.rent/logiciel-location-voiture">Lexio Rent, car rental management software</a>. When an agent prepares a quote, the counter still hands the customer a receipt for the security deposit; Lexio publishes a free <a href="https://lexio.rent/outils/recu-depot-de-garantie-location">rental security deposit receipt template</a> (in French) for that step.
|
|
106
|
+
|
|
107
|
+
- API documentation: <https://docs.lexio.rent>
|
|
108
|
+
- Python client: <https://github.com/lexio-rent-git/lexio-python>
|
|
109
|
+
- Contact: contact@lexio.rent
|
|
110
|
+
|
|
111
|
+
MIT License.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# langchain-lexio
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/langchain-lexio/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
LangChain tools for the [Lexio Rent API](https://docs.lexio.rent). They let an agent answer questions about a vehicle rental company: which vehicles are free next weekend and at what price, which bookings start tomorrow, what a customer has rented, and (optionally) prepare a priced quote.
|
|
7
|
+
|
|
8
|
+
<a href="https://lexio.rent/logiciel-location-voiture">Lexio Rent</a> is rental management software for professional car, van and two-wheeler rental companies: planning, contracts, check-in/check-out inspections, deposits and fleet maintenance.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install -U langchain-lexio
|
|
14
|
+
export LEXIO_API_KEY="lck_..."
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The API key is created in Lexio under **Settings → Developer → API tokens**. API access is part of the Business plan. There is no sandbox: test against a dedicated organization.
|
|
18
|
+
|
|
19
|
+
## Tools
|
|
20
|
+
|
|
21
|
+
| Tool | Name | What it does | Writes? |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `LexioSearchVehiclesTool` | `lexio_search_vehicles` | Active fleet with plate, model, type, status and odometer | no |
|
|
24
|
+
| `LexioCheckAvailabilityTool` | `lexio_check_availability` | Bookable vehicles for a period, with the total price | no |
|
|
25
|
+
| `LexioListRentalsTool` | `lexio_list_rentals` | Rentals by status, date window, vehicle or customer | no |
|
|
26
|
+
| `LexioGetRentalTool` | `lexio_get_rental` | One rental with all its details | no |
|
|
27
|
+
| `LexioFindCustomerTool` | `lexio_find_customer` | Customers by email or company registration number | no |
|
|
28
|
+
| `LexioListCategoriesTool` | `lexio_list_categories` | Vehicle categories (pricing classes) | no |
|
|
29
|
+
| `LexioCreateQuoteTool` | `lexio_create_quote` | Priced quote for a vehicle and a period; books nothing | quote only |
|
|
30
|
+
|
|
31
|
+
`LexioToolkit` returns the read-only tools. Pass `allow_quotes=True` to add `lexio_create_quote`. No tool creates, changes or cancels a rental, a payment or a customer record.
|
|
32
|
+
|
|
33
|
+
Dates are the rental company's local time without offset (`2026-12-14T09:00:00`). Lists are capped by `max_results` (default 20) so that tool outputs stay small.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from langchain.agents import create_agent
|
|
39
|
+
from langchain_lexio import LexioToolkit
|
|
40
|
+
|
|
41
|
+
tools = LexioToolkit(allow_quotes=True).get_tools()
|
|
42
|
+
|
|
43
|
+
agent = create_agent(
|
|
44
|
+
"anthropic:claude-sonnet-5-5",
|
|
45
|
+
tools=tools,
|
|
46
|
+
system_prompt="You help the counter staff of a car rental agency. Dates are local time.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
result = agent.invoke(
|
|
50
|
+
{"messages": [{"role": "user", "content": "Which vans are free from 14 to 16 December, and how much?"}]}
|
|
51
|
+
)
|
|
52
|
+
print(result["messages"][-1].content)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A single tool can also be called directly:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from langchain_lexio import LexioCheckAvailabilityTool
|
|
59
|
+
|
|
60
|
+
tool = LexioCheckAvailabilityTool()
|
|
61
|
+
print(tool.invoke({"start_at": "2026-12-14T09:00:00", "end_at": "2026-12-16T18:00:00", "vehicle_type": "VAN"}))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
API errors are raised as typed exceptions from the [`lexio-rent`](https://pypi.org/project/lexio-rent/) client (`NotFoundError`, `ValidationError`, `RateLimitError`...), with the Lexio error code and request id.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install -e ".[test]"
|
|
70
|
+
pytest tests/unit_tests # offline, includes the LangChain standard unit tests
|
|
71
|
+
LEXIO_API_KEY=lck_... pytest tests/integration_tests # LangChain standard integration tests, live
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## About
|
|
75
|
+
|
|
76
|
+
`langchain-lexio` is maintained by the team behind <a href="https://lexio.rent/logiciel-location-voiture">Lexio Rent, car rental management software</a>. When an agent prepares a quote, the counter still hands the customer a receipt for the security deposit; Lexio publishes a free <a href="https://lexio.rent/outils/recu-depot-de-garantie-location">rental security deposit receipt template</a> (in French) for that step.
|
|
77
|
+
|
|
78
|
+
- API documentation: <https://docs.lexio.rent>
|
|
79
|
+
- Python client: <https://github.com/lexio-rent-git/lexio-python>
|
|
80
|
+
- Contact: contact@lexio.rent
|
|
81
|
+
|
|
82
|
+
MIT License.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""LangChain integration for the Lexio Rent API."""
|
|
2
|
+
from langchain_lexio.toolkit import LexioToolkit
|
|
3
|
+
from langchain_lexio.tools import (
|
|
4
|
+
LexioCheckAvailabilityTool,
|
|
5
|
+
LexioCreateQuoteTool,
|
|
6
|
+
LexioFindCustomerTool,
|
|
7
|
+
LexioGetRentalTool,
|
|
8
|
+
LexioListCategoriesTool,
|
|
9
|
+
LexioListRentalsTool,
|
|
10
|
+
LexioSearchVehiclesTool,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"LexioToolkit",
|
|
17
|
+
"LexioSearchVehiclesTool",
|
|
18
|
+
"LexioCheckAvailabilityTool",
|
|
19
|
+
"LexioListRentalsTool",
|
|
20
|
+
"LexioGetRentalTool",
|
|
21
|
+
"LexioFindCustomerTool",
|
|
22
|
+
"LexioListCategoriesTool",
|
|
23
|
+
"LexioCreateQuoteTool",
|
|
24
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Toolkit bundling the Lexio Rent tools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from langchain_core.utils import secret_from_env
|
|
8
|
+
from langchain_core.tools import BaseTool, BaseToolkit
|
|
9
|
+
from lexio_rent import Lexio
|
|
10
|
+
from pydantic import ConfigDict, Field, SecretStr
|
|
11
|
+
|
|
12
|
+
from langchain_lexio.tools import (
|
|
13
|
+
LexioCheckAvailabilityTool,
|
|
14
|
+
LexioCreateQuoteTool,
|
|
15
|
+
LexioFindCustomerTool,
|
|
16
|
+
LexioGetRentalTool,
|
|
17
|
+
LexioListCategoriesTool,
|
|
18
|
+
LexioListRentalsTool,
|
|
19
|
+
LexioSearchVehiclesTool,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
READ_TOOLS = (
|
|
23
|
+
LexioSearchVehiclesTool,
|
|
24
|
+
LexioCheckAvailabilityTool,
|
|
25
|
+
LexioListRentalsTool,
|
|
26
|
+
LexioGetRentalTool,
|
|
27
|
+
LexioFindCustomerTool,
|
|
28
|
+
LexioListCategoriesTool,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LexioToolkit(BaseToolkit):
|
|
33
|
+
"""Lexio Rent tools for agents.
|
|
34
|
+
|
|
35
|
+
Read-only by default: fleet, availability and prices, rentals, customers and
|
|
36
|
+
categories. ``allow_quotes=True`` adds ``lexio_create_quote``, which writes a
|
|
37
|
+
quote record but books nothing. Rentals, payments and customer records are
|
|
38
|
+
never created or changed by this toolkit.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
42
|
+
|
|
43
|
+
api_key: Optional[SecretStr] = Field(
|
|
44
|
+
default_factory=secret_from_env("LEXIO_API_KEY", default=None),
|
|
45
|
+
description="Lexio API key (lck_...). Defaults to LEXIO_API_KEY.",
|
|
46
|
+
)
|
|
47
|
+
base_url: Optional[str] = None
|
|
48
|
+
max_results: int = 20
|
|
49
|
+
allow_quotes: bool = False
|
|
50
|
+
client: Optional[Any] = Field(default=None, exclude=True)
|
|
51
|
+
|
|
52
|
+
def _client(self) -> Lexio:
|
|
53
|
+
if self.client is None:
|
|
54
|
+
key = self.api_key.get_secret_value() if self.api_key else os.environ.get("LEXIO_API_KEY")
|
|
55
|
+
self.client = Lexio(api_key=key, base_url=self.base_url)
|
|
56
|
+
return self.client
|
|
57
|
+
|
|
58
|
+
def get_tools(self) -> list[BaseTool]:
|
|
59
|
+
client = self._client()
|
|
60
|
+
classes = list(READ_TOOLS) + ([LexioCreateQuoteTool] if self.allow_quotes else [])
|
|
61
|
+
return [cls(client=client, max_results=self.max_results) for cls in classes]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""LangChain tools for the Lexio Rent API (car and van rental management software)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from langchain_core.callbacks import CallbackManagerForToolRun
|
|
9
|
+
from langchain_core.utils import secret_from_env
|
|
10
|
+
from langchain_core.tools import BaseTool
|
|
11
|
+
from lexio_rent import Lexio
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, SecretStr
|
|
13
|
+
|
|
14
|
+
VEHICLE_TYPES = "CITY, COMPACT, CROSSOVER, SEDAN, BREAK, SUV, OFF_ROAD, PICKUP, MINIVAN, VAN, UTILITY, COUPE, CONVERTIBLE, SPORT, LUXURY, MOTORCYCLE, SCOOTER"
|
|
15
|
+
RENTAL_STATUSES = "PENDING_APPROVAL, RESERVED, IN_PROGRESS, COMPLETED, CANCELLED"
|
|
16
|
+
LOCAL_TIME = "Local date-time of the rental company, without offset, e.g. 2026-12-14T09:00:00."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _dump(value: Any) -> str:
|
|
20
|
+
return json.dumps(value, ensure_ascii=False, default=str)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _pick(obj: Optional[dict], *keys: str) -> Optional[dict]:
|
|
24
|
+
if not isinstance(obj, dict):
|
|
25
|
+
return obj
|
|
26
|
+
return {k: obj.get(k) for k in keys if k in obj}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LexioBaseTool(BaseTool):
|
|
30
|
+
"""Shared settings: API key (``LEXIO_API_KEY``), base URL and client."""
|
|
31
|
+
|
|
32
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
33
|
+
|
|
34
|
+
api_key: Optional[SecretStr] = Field(
|
|
35
|
+
default_factory=secret_from_env("LEXIO_API_KEY", default=None),
|
|
36
|
+
description="Lexio API key (lck_...). Defaults to LEXIO_API_KEY.",
|
|
37
|
+
)
|
|
38
|
+
base_url: Optional[str] = Field(default=None, description="API base URL. Defaults to LEXIO_BASE_URL or production.")
|
|
39
|
+
max_results: int = Field(default=20, description="Upper bound on items returned to the model.")
|
|
40
|
+
client: Optional[Any] = Field(default=None, exclude=True, description="Optional shared lexio_rent.Lexio client.")
|
|
41
|
+
_own_client: Optional[Lexio] = PrivateAttr(default=None)
|
|
42
|
+
|
|
43
|
+
def _lexio(self) -> Lexio:
|
|
44
|
+
if self.client is not None:
|
|
45
|
+
return self.client
|
|
46
|
+
if self._own_client is None:
|
|
47
|
+
key = self.api_key.get_secret_value() if self.api_key else os.environ.get("LEXIO_API_KEY")
|
|
48
|
+
self._own_client = Lexio(api_key=key, base_url=self.base_url)
|
|
49
|
+
return self._own_client
|
|
50
|
+
|
|
51
|
+
def _list(self, method: Any, limit: Optional[int], **filters: Any) -> list:
|
|
52
|
+
cap = min(limit or self.max_results, self.max_results)
|
|
53
|
+
out = []
|
|
54
|
+
for item in self._lexio().paginate(method, limit=min(cap, 100), **filters):
|
|
55
|
+
out.append(item)
|
|
56
|
+
if len(out) >= cap:
|
|
57
|
+
break
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- search vehicles -------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
class SearchVehiclesInput(BaseModel):
|
|
64
|
+
category_id: Optional[str] = Field(default=None, description="Only vehicles of this category id.")
|
|
65
|
+
vehicle_type: Optional[str] = Field(default=None, description="Vehicle type, one of: " + VEHICLE_TYPES + ".")
|
|
66
|
+
status: Optional[str] = Field(default=None, description="OPERATIONAL or BREAKDOWN.")
|
|
67
|
+
limit: Optional[int] = Field(default=None, description="Maximum number of vehicles to return.")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LexioSearchVehiclesTool(LexioBaseTool):
|
|
71
|
+
"""List the active fleet of the rental company."""
|
|
72
|
+
|
|
73
|
+
name: str = "lexio_search_vehicles"
|
|
74
|
+
description: str = (
|
|
75
|
+
"List vehicles of the rental company fleet (active, not archived) with plate, model, category, "
|
|
76
|
+
"status and odometer. Use it to find a vehicle id."
|
|
77
|
+
)
|
|
78
|
+
args_schema: type[BaseModel] = SearchVehiclesInput
|
|
79
|
+
|
|
80
|
+
def _run(
|
|
81
|
+
self,
|
|
82
|
+
category_id: Optional[str] = None,
|
|
83
|
+
vehicle_type: Optional[str] = None,
|
|
84
|
+
status: Optional[str] = None,
|
|
85
|
+
limit: Optional[int] = None,
|
|
86
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
87
|
+
) -> str:
|
|
88
|
+
items = self._list(
|
|
89
|
+
self._lexio().vehicles.list,
|
|
90
|
+
limit,
|
|
91
|
+
is_archived=False,
|
|
92
|
+
category_id=category_id,
|
|
93
|
+
vehicle_type=vehicle_type,
|
|
94
|
+
status=status,
|
|
95
|
+
)
|
|
96
|
+
keys = ("id", "plate", "name", "brand", "model", "vehicleType", "status", "odometerKm", "category")
|
|
97
|
+
return _dump([_pick(v, *keys) for v in items])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- availability ----------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
class CheckAvailabilityInput(BaseModel):
|
|
103
|
+
start_at: str = Field(description="Pick-up. " + LOCAL_TIME)
|
|
104
|
+
end_at: str = Field(description="Return. " + LOCAL_TIME)
|
|
105
|
+
category_id: Optional[str] = Field(default=None, description="Only this category id.")
|
|
106
|
+
vehicle_type: Optional[str] = Field(default=None, description="Vehicle type, one of: " + VEHICLE_TYPES + ".")
|
|
107
|
+
only_available: bool = Field(default=True, description="True: bookable vehicles only. False: the others, with reasons.")
|
|
108
|
+
limit: Optional[int] = Field(default=None, description="Maximum number of offers to return.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class LexioCheckAvailabilityTool(LexioBaseTool):
|
|
112
|
+
"""Bookable vehicles and their price for a period."""
|
|
113
|
+
|
|
114
|
+
name: str = "lexio_check_availability"
|
|
115
|
+
description: str = (
|
|
116
|
+
"Check which vehicles can be booked for a period and their total price (tax included, with the default "
|
|
117
|
+
"mileage package, excess and deposit). Prices are indicative; only a quote fixes the price."
|
|
118
|
+
)
|
|
119
|
+
args_schema: type[BaseModel] = CheckAvailabilityInput
|
|
120
|
+
|
|
121
|
+
def _run(
|
|
122
|
+
self,
|
|
123
|
+
start_at: str,
|
|
124
|
+
end_at: str,
|
|
125
|
+
category_id: Optional[str] = None,
|
|
126
|
+
vehicle_type: Optional[str] = None,
|
|
127
|
+
only_available: bool = True,
|
|
128
|
+
limit: Optional[int] = None,
|
|
129
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
130
|
+
) -> str:
|
|
131
|
+
offers = self._list(
|
|
132
|
+
self._lexio().availability.list,
|
|
133
|
+
limit,
|
|
134
|
+
start_at=start_at,
|
|
135
|
+
end_at=end_at,
|
|
136
|
+
category_id=category_id,
|
|
137
|
+
vehicle_type=vehicle_type,
|
|
138
|
+
is_available=only_available,
|
|
139
|
+
)
|
|
140
|
+
out = []
|
|
141
|
+
for o in offers:
|
|
142
|
+
out.append(
|
|
143
|
+
{
|
|
144
|
+
"vehicle": _pick(o.get("vehicle"), "id", "name", "brand", "model", "plate", "vehicleType"),
|
|
145
|
+
"isAvailable": o.get("isAvailable"),
|
|
146
|
+
"unavailableReasons": o.get("unavailableReasons"),
|
|
147
|
+
"billedDays": (o.get("period") or {}).get("billedDays"),
|
|
148
|
+
"currency": o.get("currency"),
|
|
149
|
+
"totalTTC": o.get("totalTTC"),
|
|
150
|
+
"mileage": _pick(o.get("mileage"), "name", "isUnlimited", "includedKm"),
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
return _dump(out)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --- rentals ---------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
class ListRentalsInput(BaseModel):
|
|
159
|
+
status: Optional[str] = Field(default=None, description=f"Comma-separated statuses: {RENTAL_STATUSES}.")
|
|
160
|
+
from_date: Optional[str] = Field(default=None, description="First day (YYYY-MM-DD) of the window, included.")
|
|
161
|
+
to_date: Optional[str] = Field(default=None, description="Last day (YYYY-MM-DD) of the window, included.")
|
|
162
|
+
vehicle_id: Optional[str] = Field(default=None, description="Only rentals of this vehicle id.")
|
|
163
|
+
customer_id: Optional[str] = Field(default=None, description="Only rentals of this customer id.")
|
|
164
|
+
limit: Optional[int] = Field(default=None, description="Maximum number of rentals to return.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class LexioListRentalsTool(LexioBaseTool):
|
|
168
|
+
"""Search rentals (bookings)."""
|
|
169
|
+
|
|
170
|
+
name: str = "lexio_list_rentals"
|
|
171
|
+
description: str = (
|
|
172
|
+
"Search rentals (bookings) by status, date window, vehicle or customer. Returns number, status, period, "
|
|
173
|
+
"vehicle, customer and totals. Without status, cancelled rentals are included."
|
|
174
|
+
)
|
|
175
|
+
args_schema: type[BaseModel] = ListRentalsInput
|
|
176
|
+
|
|
177
|
+
def _run(
|
|
178
|
+
self,
|
|
179
|
+
status: Optional[str] = None,
|
|
180
|
+
from_date: Optional[str] = None,
|
|
181
|
+
to_date: Optional[str] = None,
|
|
182
|
+
vehicle_id: Optional[str] = None,
|
|
183
|
+
customer_id: Optional[str] = None,
|
|
184
|
+
limit: Optional[int] = None,
|
|
185
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
186
|
+
) -> str:
|
|
187
|
+
items = self._list(
|
|
188
|
+
self._lexio().rentals.list,
|
|
189
|
+
limit,
|
|
190
|
+
status=status,
|
|
191
|
+
from_=from_date,
|
|
192
|
+
to=to_date,
|
|
193
|
+
vehicle_id=vehicle_id,
|
|
194
|
+
customer_id=customer_id,
|
|
195
|
+
)
|
|
196
|
+
return _dump([_compact_rental(r) for r in items])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _compact_rental(r: dict) -> dict:
|
|
200
|
+
return {
|
|
201
|
+
"id": r.get("id"),
|
|
202
|
+
"number": r.get("number"),
|
|
203
|
+
"status": r.get("status"),
|
|
204
|
+
"period": _pick(r.get("period"), "startAt", "endAt", "timezone"),
|
|
205
|
+
"vehicle": _pick(r.get("vehicle"), "id", "name", "plate"),
|
|
206
|
+
"customer": _pick(r.get("customer"), "id", "firstName", "lastName", "companyName", "email"),
|
|
207
|
+
"currency": r.get("currency"),
|
|
208
|
+
"totalTTC": r.get("totalTTC"),
|
|
209
|
+
"balance": r.get("balance"),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class GetRentalInput(BaseModel):
|
|
214
|
+
rental_id: str = Field(description="Rental id (UUID).")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class LexioGetRentalTool(LexioBaseTool):
|
|
218
|
+
"""One rental with all its details."""
|
|
219
|
+
|
|
220
|
+
name: str = "lexio_get_rental"
|
|
221
|
+
description: str = (
|
|
222
|
+
"Get one rental by id with all details: period, actual pick-up and return, vehicle, customer, locations, "
|
|
223
|
+
"mileage package, excess, deposit, options, price lines, balance and notes."
|
|
224
|
+
)
|
|
225
|
+
args_schema: type[BaseModel] = GetRentalInput
|
|
226
|
+
|
|
227
|
+
def _run(self, rental_id: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
|
228
|
+
return _dump(self._lexio().rentals.get(rental_id)["data"])
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# --- customers -------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
class FindCustomerInput(BaseModel):
|
|
234
|
+
email: Optional[str] = Field(default=None, description="Exact email address of the customer.")
|
|
235
|
+
company_registration_number: Optional[str] = Field(default=None, description="Company registration number (business customers).")
|
|
236
|
+
limit: Optional[int] = Field(default=None, description="Maximum number of customers to return.")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class LexioFindCustomerTool(LexioBaseTool):
|
|
240
|
+
"""Find customers by email or company registration number."""
|
|
241
|
+
|
|
242
|
+
name: str = "lexio_find_customer"
|
|
243
|
+
description: str = "Find customers of the rental company by exact email or company registration number."
|
|
244
|
+
args_schema: type[BaseModel] = FindCustomerInput
|
|
245
|
+
|
|
246
|
+
def _run(
|
|
247
|
+
self,
|
|
248
|
+
email: Optional[str] = None,
|
|
249
|
+
company_registration_number: Optional[str] = None,
|
|
250
|
+
limit: Optional[int] = None,
|
|
251
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
252
|
+
) -> str:
|
|
253
|
+
items = self._list(
|
|
254
|
+
self._lexio().customers.list, limit, email=email, company_registration_number=company_registration_number
|
|
255
|
+
)
|
|
256
|
+
return _dump(items)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# --- catalogue -------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
class ListCategoriesInput(BaseModel):
|
|
262
|
+
limit: Optional[int] = Field(default=None, description="Maximum number of categories to return.")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class LexioListCategoriesTool(LexioBaseTool):
|
|
266
|
+
"""Vehicle categories (pricing classes)."""
|
|
267
|
+
|
|
268
|
+
name: str = "lexio_list_categories"
|
|
269
|
+
description: str = "List the vehicle categories (pricing classes) of the rental company, with their ids."
|
|
270
|
+
args_schema: type[BaseModel] = ListCategoriesInput
|
|
271
|
+
|
|
272
|
+
def _run(self, limit: Optional[int] = None, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
|
273
|
+
return _dump(self._list(self._lexio().categories.list, limit))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# --- quote (write, opt-in) -------------------------------------------------
|
|
277
|
+
|
|
278
|
+
class CreateQuoteInput(BaseModel):
|
|
279
|
+
vehicle_id: str = Field(description="Vehicle id, from lexio_check_availability.")
|
|
280
|
+
start_at: str = Field(description="Pick-up. " + LOCAL_TIME)
|
|
281
|
+
end_at: str = Field(description="Return. " + LOCAL_TIME)
|
|
282
|
+
mileage_package_id: Optional[str] = Field(default=None, description="Mileage package id; default package if omitted.")
|
|
283
|
+
deductible_id: Optional[str] = Field(default=None, description="Excess (deductible) id; default if omitted.")
|
|
284
|
+
deposit_id: Optional[str] = Field(default=None, description="Deposit id; default if omitted.")
|
|
285
|
+
promo_code: Optional[str] = Field(default=None, description="Promo code, if the customer has one.")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class LexioCreateQuoteTool(LexioBaseTool):
|
|
289
|
+
"""Create a priced quote (writes to Lexio, books nothing)."""
|
|
290
|
+
|
|
291
|
+
name: str = "lexio_create_quote"
|
|
292
|
+
description: str = (
|
|
293
|
+
"Create a quote that fixes the price of a vehicle for a period. It writes a quote record in Lexio but does "
|
|
294
|
+
"not book the vehicle. Returns the quote id, total and expiry."
|
|
295
|
+
)
|
|
296
|
+
args_schema: type[BaseModel] = CreateQuoteInput
|
|
297
|
+
|
|
298
|
+
def _run(
|
|
299
|
+
self,
|
|
300
|
+
vehicle_id: str,
|
|
301
|
+
start_at: str,
|
|
302
|
+
end_at: str,
|
|
303
|
+
mileage_package_id: Optional[str] = None,
|
|
304
|
+
deductible_id: Optional[str] = None,
|
|
305
|
+
deposit_id: Optional[str] = None,
|
|
306
|
+
promo_code: Optional[str] = None,
|
|
307
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
308
|
+
) -> str:
|
|
309
|
+
body = {"vehicleId": vehicle_id, "startAt": start_at, "endAt": end_at}
|
|
310
|
+
optional = {
|
|
311
|
+
"mileagePackageId": mileage_package_id,
|
|
312
|
+
"deductibleId": deductible_id,
|
|
313
|
+
"depositId": deposit_id,
|
|
314
|
+
"promoCode": promo_code,
|
|
315
|
+
}
|
|
316
|
+
body.update({k: v for k, v in optional.items() if v is not None})
|
|
317
|
+
return _dump(self._lexio().quotes.create(body)["data"])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-lexio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain tools for the Lexio Rent API (car rental software): fleet, availability and prices, rentals, customers and quotes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Lexio Rent", email = "contact@lexio.rent" }]
|
|
14
|
+
keywords = ["langchain", "langchain-tools", "agents", "lexio", "lexio-rent", "car-rental", "fleet-management"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Office/Business",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["langchain-core>=0.3,<2", "lexio-rent>=0.1.1", "pydantic>=2"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest>=7", "pytest-asyncio", "langchain-tests>=0.3"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://lexio.rent/logiciel-location-voiture"
|
|
30
|
+
Documentation = "https://github.com/lexio-rent-git/langchain-lexio#readme"
|
|
31
|
+
"API reference" = "https://docs.lexio.rent"
|
|
32
|
+
Source = "https://github.com/lexio-rent-git/langchain-lexio"
|
|
33
|
+
Issues = "https://github.com/lexio-rent-git/langchain-lexio/issues"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["langchain_lexio"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["langchain_lexio", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""LangChain standard integration tests against a real Lexio organization (needs LEXIO_API_KEY)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from langchain_tests.integration_tests import ToolsIntegrationTests
|
|
9
|
+
|
|
10
|
+
from langchain_lexio import LexioCheckAvailabilityTool, LexioListRentalsTool, LexioSearchVehiclesTool, LexioToolkit
|
|
11
|
+
|
|
12
|
+
pytestmark = pytest.mark.skipif(not os.environ.get("LEXIO_API_KEY"), reason="set LEXIO_API_KEY to run live tests")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestSearchVehiclesLive(ToolsIntegrationTests):
|
|
16
|
+
@property
|
|
17
|
+
def tool_constructor(self):
|
|
18
|
+
return LexioSearchVehiclesTool
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def tool_invoke_params_example(self) -> dict:
|
|
22
|
+
return {"limit": 3}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestCheckAvailabilityLive(ToolsIntegrationTests):
|
|
26
|
+
@property
|
|
27
|
+
def tool_constructor(self):
|
|
28
|
+
return LexioCheckAvailabilityTool
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def tool_invoke_params_example(self) -> dict:
|
|
32
|
+
return {"start_at": "2027-03-01T09:00:00", "end_at": "2027-03-03T18:00:00", "limit": 3}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestListRentalsLive(ToolsIntegrationTests):
|
|
36
|
+
@property
|
|
37
|
+
def tool_constructor(self):
|
|
38
|
+
return LexioListRentalsTool
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def tool_invoke_params_example(self) -> dict:
|
|
42
|
+
return {"status": "RESERVED,IN_PROGRESS", "limit": 3}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_toolkit_end_to_end():
|
|
46
|
+
tools = {t.name: t for t in LexioToolkit().get_tools()}
|
|
47
|
+
vehicles = json.loads(tools["lexio_search_vehicles"].invoke({"limit": 2}))
|
|
48
|
+
assert vehicles and vehicles[0]["id"]
|
|
49
|
+
rentals = json.loads(tools["lexio_list_rentals"].invoke({"limit": 1}))
|
|
50
|
+
if rentals:
|
|
51
|
+
full = json.loads(tools["lexio_get_rental"].invoke({"rental_id": rentals[0]["id"]}))
|
|
52
|
+
assert full["id"] == rentals[0]["id"]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Offline tests: LangChain standard unit tests + behaviour against a mocked API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import pytest
|
|
8
|
+
from langchain_tests.unit_tests import ToolsUnitTests
|
|
9
|
+
from lexio_rent import Lexio
|
|
10
|
+
|
|
11
|
+
from langchain_lexio import (
|
|
12
|
+
LexioCheckAvailabilityTool,
|
|
13
|
+
LexioCreateQuoteTool,
|
|
14
|
+
LexioGetRentalTool,
|
|
15
|
+
LexioListRentalsTool,
|
|
16
|
+
LexioSearchVehiclesTool,
|
|
17
|
+
LexioToolkit,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fake_client(handler, seen):
|
|
22
|
+
def wrapped(req):
|
|
23
|
+
seen.append(req)
|
|
24
|
+
return handler(req)
|
|
25
|
+
|
|
26
|
+
return Lexio("lck_test", http_client=httpx.Client(transport=httpx.MockTransport(wrapped)), max_retries=0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Std(ToolsUnitTests):
|
|
30
|
+
@property
|
|
31
|
+
def tool_constructor_params(self) -> dict:
|
|
32
|
+
return {"api_key": "lck_test"}
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def init_from_env_params(self):
|
|
36
|
+
return ({"LEXIO_API_KEY": "lck_env"}, {}, {"api_key": "lck_env"})
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestSearchVehiclesStandard(_Std):
|
|
40
|
+
@property
|
|
41
|
+
def tool_constructor(self):
|
|
42
|
+
return LexioSearchVehiclesTool
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def tool_invoke_params_example(self) -> dict:
|
|
46
|
+
return {"vehicle_type": "SUV", "limit": 5}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestCheckAvailabilityStandard(_Std):
|
|
50
|
+
@property
|
|
51
|
+
def tool_constructor(self):
|
|
52
|
+
return LexioCheckAvailabilityTool
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def tool_invoke_params_example(self) -> dict:
|
|
56
|
+
return {"start_at": "2026-12-14T09:00:00", "end_at": "2026-12-16T18:00:00"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestGetRentalStandard(_Std):
|
|
60
|
+
@property
|
|
61
|
+
def tool_constructor(self):
|
|
62
|
+
return LexioGetRentalTool
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def tool_invoke_params_example(self) -> dict:
|
|
66
|
+
return {"rental_id": "9c2d7e4a-5b1f-4d3c-8a6e-0f1b2c3d4e5f"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_toolkit_is_read_only_by_default():
|
|
70
|
+
names = [t.name for t in LexioToolkit(api_key="lck_test").get_tools()]
|
|
71
|
+
assert "lexio_create_quote" not in names
|
|
72
|
+
assert names == [
|
|
73
|
+
"lexio_search_vehicles",
|
|
74
|
+
"lexio_check_availability",
|
|
75
|
+
"lexio_list_rentals",
|
|
76
|
+
"lexio_get_rental",
|
|
77
|
+
"lexio_find_customer",
|
|
78
|
+
"lexio_list_categories",
|
|
79
|
+
]
|
|
80
|
+
assert "lexio_create_quote" in [t.name for t in LexioToolkit(api_key="lck_test", allow_quotes=True).get_tools()]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_toolkit_shares_one_client():
|
|
84
|
+
tools = LexioToolkit(api_key="lck_test").get_tools()
|
|
85
|
+
assert len({id(t.client) for t in tools}) == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_search_vehicles_filters_and_compacts():
|
|
89
|
+
seen = []
|
|
90
|
+
|
|
91
|
+
def handler(req):
|
|
92
|
+
return httpx.Response(200, json={"data": [{"id": "v1", "plate": "AB-123-CD", "vin": "secret-ish", "status": "AVAILABLE"}], "nextCursor": None})
|
|
93
|
+
|
|
94
|
+
tool = LexioSearchVehiclesTool(client=fake_client(handler, seen))
|
|
95
|
+
out = json.loads(tool.invoke({"vehicle_type": "VAN"}))
|
|
96
|
+
assert out == [{"id": "v1", "plate": "AB-123-CD", "status": "AVAILABLE"}]
|
|
97
|
+
url = str(seen[0].url)
|
|
98
|
+
assert "isArchived=false" in url and "vehicleType=VAN" in url
|
|
99
|
+
assert seen[0].headers["authorization"] == "Bearer lck_test"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_max_results_caps_pagination():
|
|
103
|
+
seen = []
|
|
104
|
+
pages = {None: {"data": [{"id": i} for i in range(3)], "nextCursor": "c2"}, "c2": {"data": [{"id": 9}], "nextCursor": None}}
|
|
105
|
+
|
|
106
|
+
def handler(req):
|
|
107
|
+
return httpx.Response(200, json=pages[req.url.params.get("cursor")])
|
|
108
|
+
|
|
109
|
+
tool = LexioSearchVehiclesTool(client=fake_client(handler, seen), max_results=2)
|
|
110
|
+
assert len(json.loads(tool.invoke({}))) == 2
|
|
111
|
+
assert len(seen) == 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_check_availability_params():
|
|
115
|
+
seen = []
|
|
116
|
+
|
|
117
|
+
def handler(req):
|
|
118
|
+
return httpx.Response(200, json={"data": [{"vehicle": {"id": "v1", "name": "Clio"}, "isAvailable": True, "totalTTC": 120.5, "currency": "EUR", "period": {"billedDays": 3}}], "nextCursor": None})
|
|
119
|
+
|
|
120
|
+
tool = LexioCheckAvailabilityTool(client=fake_client(handler, seen))
|
|
121
|
+
out = json.loads(tool.invoke({"start_at": "2026-12-14T09:00:00", "end_at": "2026-12-16T18:00:00"}))
|
|
122
|
+
assert out[0]["totalTTC"] == 120.5 and out[0]["billedDays"] == 3
|
|
123
|
+
params = seen[0].url.params
|
|
124
|
+
assert params["startAt"] == "2026-12-14T09:00:00" and params["isAvailable"] == "true"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_list_rentals_maps_date_window():
|
|
128
|
+
seen = []
|
|
129
|
+
tool = LexioListRentalsTool(client=fake_client(lambda r: httpx.Response(200, json={"data": [], "nextCursor": None}), seen))
|
|
130
|
+
assert json.loads(tool.invoke({"status": "RESERVED", "from_date": "2026-12-01", "to_date": "2026-12-31"})) == []
|
|
131
|
+
params = seen[0].url.params
|
|
132
|
+
assert params["from"] == "2026-12-01" and params["to"] == "2026-12-31" and params["status"] == "RESERVED"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_create_quote_body_and_idempotency():
|
|
136
|
+
seen = []
|
|
137
|
+
|
|
138
|
+
def handler(req):
|
|
139
|
+
return httpx.Response(201, json={"data": {"id": "q1", "totalTTC": 99}})
|
|
140
|
+
|
|
141
|
+
tool = LexioCreateQuoteTool(client=fake_client(handler, seen))
|
|
142
|
+
out = json.loads(tool.invoke({"vehicle_id": "v1", "start_at": "2026-12-14T09:00:00", "end_at": "2026-12-16T18:00:00"}))
|
|
143
|
+
assert out["id"] == "q1"
|
|
144
|
+
assert json.loads(seen[0].content) == {"vehicleId": "v1", "startAt": "2026-12-14T09:00:00", "endAt": "2026-12-16T18:00:00"}
|
|
145
|
+
assert seen[0].method == "POST"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_api_errors_surface():
|
|
149
|
+
from lexio_rent import NotFoundError
|
|
150
|
+
|
|
151
|
+
tool = LexioGetRentalTool(client=fake_client(lambda r: httpx.Response(404, json={"code": "NOT_FOUND", "detail": "x"}), []))
|
|
152
|
+
with pytest.raises(NotFoundError):
|
|
153
|
+
tool.invoke({"rental_id": "nope"})
|