vortexa-claude-skills 1.0.0
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.
- package/CHANGELOG.md +28 -0
- package/VERSION +1 -0
- package/bin/.gitkeep +0 -0
- package/bin/setup.js +302 -0
- package/commands/vortexa/_check-setup.md +9 -0
- package/commands/vortexa/_skill-template.md +100 -0
- package/commands/vortexa/breakdown.md +294 -0
- package/commands/vortexa/cargo-flows.md +247 -0
- package/commands/vortexa/compare.md +315 -0
- package/commands/vortexa/custom.md +214 -0
- package/commands/vortexa/explain.md +124 -0
- package/commands/vortexa/init.md +133 -0
- package/commands/vortexa/oow.md +189 -0
- package/commands/vortexa/seasonal.md +185 -0
- package/commands/vortexa/voyages.md +285 -0
- package/context/.gitkeep +0 -0
- package/context/cargo-movements.md +738 -0
- package/context/date-units.md +188 -0
- package/context/endpoint-template.md +176 -0
- package/context/entity-resolution.md +217 -0
- package/context/guardrails.md +161 -0
- package/context/reference-endpoints.md +651 -0
- package/context/voyages.md +636 -0
- package/lib/__init__.py +4 -0
- package/lib/aliases.json +52 -0
- package/lib/api.py +20 -0
- package/lib/entities.py +254 -0
- package/lib/inventory.py +140 -0
- package/lib/movements.py +242 -0
- package/lib/requirements.txt +6 -0
- package/lib/seasonal.py +200 -0
- package/lib/timeseries.py +271 -0
- package/lib/utils.py +120 -0
- package/lib/vessels.py +192 -0
- package/lib/visualization.py +164 -0
- package/lib/voyages.py +236 -0
- package/package.json +28 -0
- package/templates/.env.template +3 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Date Ranges & Units
|
|
2
|
+
|
|
3
|
+
> Date parsing conventions and unit selection rules for all Vortexa queries.
|
|
4
|
+
> Loaded automatically for every /vortexa: skill.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Date Parsing Convention
|
|
9
|
+
|
|
10
|
+
Two distinct patterns: **calendar periods** (named references to fixed ranges) and **trailing periods** (rolling windows from today).
|
|
11
|
+
|
|
12
|
+
**Rule:** "last" + named period = calendar. "last" + number + period = trailing.
|
|
13
|
+
|
|
14
|
+
### Calendar Periods (Named References)
|
|
15
|
+
|
|
16
|
+
| User Says | Start | End |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| "last year" | Jan 1 of previous year, 00:00:00 | Dec 31 of previous year, 23:59:59 |
|
|
19
|
+
| "last month" | 1st of previous month, 00:00:00 | Last day of previous month, 23:59:59 |
|
|
20
|
+
| "last quarter" | 1st of previous quarter, 00:00:00 | Last day of previous quarter, 23:59:59 |
|
|
21
|
+
| "Q3 2025" | Jul 1 2025, 00:00:00 | Sep 30 2025, 23:59:59 |
|
|
22
|
+
| "Q1 2026" | Jan 1 2026, 00:00:00 | Mar 31 2026, 23:59:59 |
|
|
23
|
+
| "2024" | Jan 1 2024, 00:00:00 | Dec 31 2024, 23:59:59 |
|
|
24
|
+
| "2025" | Jan 1 2025, 00:00:00 | Dec 31 2025, 23:59:59 |
|
|
25
|
+
| "YTD", "year to date" | Jan 1 of current year, 00:00:00 | Current timestamp |
|
|
26
|
+
| "this month" | 1st of current month, 00:00:00 | Current timestamp |
|
|
27
|
+
| "this quarter" | 1st of current quarter, 00:00:00 | Current timestamp |
|
|
28
|
+
|
|
29
|
+
### Quarter Reference
|
|
30
|
+
|
|
31
|
+
| Quarter | Start | End |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| Q1 | Jan 1 | Mar 31 |
|
|
34
|
+
| Q2 | Apr 1 | Jun 30 |
|
|
35
|
+
| Q3 | Jul 1 | Sep 30 |
|
|
36
|
+
| Q4 | Oct 1 | Dec 31 |
|
|
37
|
+
|
|
38
|
+
### Trailing Periods (Number + Unit)
|
|
39
|
+
|
|
40
|
+
| User Says | Start | End |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| "last 6 months" | Today minus 6 months, 00:00:00 | Current timestamp |
|
|
43
|
+
| "last 30 days" | Today minus 30 days, 00:00:00 | Current timestamp |
|
|
44
|
+
| "last 12 months" | Today minus 12 months, 00:00:00 | Current timestamp |
|
|
45
|
+
| "last 2 weeks" | Today minus 14 days, 00:00:00 | Current timestamp |
|
|
46
|
+
| "rolling 3 months" | Today minus 90 days, 00:00:00 | Current timestamp |
|
|
47
|
+
|
|
48
|
+
### CRITICAL DATE RULES
|
|
49
|
+
|
|
50
|
+
**NEVER** leave end dates at midnight (00:00:00) -- data from the last day will be excluded entirely. This is the most common silent data loss bug.
|
|
51
|
+
|
|
52
|
+
**ALWAYS** use `datetime(year, month, day, 23, 59, 59)` for historical end dates.
|
|
53
|
+
|
|
54
|
+
**ALWAYS** specify `filter_time_min` and `filter_time_max` explicitly -- never rely on defaults.
|
|
55
|
+
|
|
56
|
+
### Code Examples
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from datetime import datetime
|
|
60
|
+
|
|
61
|
+
# "last year" (calendar) -- assuming current year is 2026
|
|
62
|
+
time_min = datetime(2025, 1, 1) # Jan 1, 00:00:00
|
|
63
|
+
time_max = datetime(2025, 12, 31, 23, 59, 59) # Dec 31, 23:59:59
|
|
64
|
+
|
|
65
|
+
# "January 2025"
|
|
66
|
+
time_min = datetime(2025, 1, 1)
|
|
67
|
+
time_max = datetime(2025, 1, 31, 23, 59, 59)
|
|
68
|
+
|
|
69
|
+
# "Q3 2025"
|
|
70
|
+
time_min = datetime(2025, 7, 1)
|
|
71
|
+
time_max = datetime(2025, 9, 30, 23, 59, 59)
|
|
72
|
+
|
|
73
|
+
# "last 6 months" (trailing) -- rolling from today
|
|
74
|
+
from dateutil.relativedelta import relativedelta
|
|
75
|
+
time_min = datetime.now() - relativedelta(months=6)
|
|
76
|
+
time_max = datetime.now()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Frequency Selection
|
|
82
|
+
|
|
83
|
+
Frequency determines the time bucket size for timeseries queries.
|
|
84
|
+
|
|
85
|
+
### Frequency Options
|
|
86
|
+
|
|
87
|
+
| User Says | API Value (CM/CTS) | API Value (Voyages) | Description |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| "daily" | `day` | `day` | One data point per day |
|
|
90
|
+
| "weekly" | `week` | `week` | One data point per week |
|
|
91
|
+
| "DOE week" | `doe_week` | `doe_week` | US EIA/DOE week convention |
|
|
92
|
+
| "monthly" | `month` | `month` | One data point per month |
|
|
93
|
+
| "quarterly" | `quarter` | `quarter` | One data point per quarter |
|
|
94
|
+
| "yearly", "annually" | `year` | `year` | One data point per year |
|
|
95
|
+
|
|
96
|
+
### Frequency Guidance by Date Range
|
|
97
|
+
|
|
98
|
+
| Date Range Length | Suggested Frequency | Rationale |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| < 1 month | `day` | Daily granularity is useful for short windows |
|
|
101
|
+
| 1-3 months | `day` or `week` | Weekly smooths noise; daily for detail |
|
|
102
|
+
| 3-12 months | `week` or `month` | Monthly is cleaner for medium ranges |
|
|
103
|
+
| 1-3 years | `month` | Monthly avoids excessive data points |
|
|
104
|
+
| > 3 years | `month` or `quarter` | Quarterly for very long ranges |
|
|
105
|
+
|
|
106
|
+
### Frequency Rules
|
|
107
|
+
|
|
108
|
+
**ALWAYS** ask the user what frequency they want -- unless they already specified it in the query.
|
|
109
|
+
|
|
110
|
+
If the user says "monthly crude exports" -- "monthly" IS the frequency. No need to ask.
|
|
111
|
+
|
|
112
|
+
If the user says "crude exports from Saudi Arabia last year" -- frequency is NOT specified. Ask: "What frequency would you like? daily / weekly / monthly?"
|
|
113
|
+
|
|
114
|
+
The guidance table above is for suggesting defaults, not auto-selecting. Present it as a recommendation.
|
|
115
|
+
|
|
116
|
+
For CargoTimeSeries, the parameter is `timeseries_frequency`.
|
|
117
|
+
For VoyagesTimeseries, the parameter is `breakdown_frequency`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Unit Defaults by Commodity
|
|
122
|
+
|
|
123
|
+
### Default Unit Table
|
|
124
|
+
|
|
125
|
+
| Product Group | Default Unit | API Value | Reason |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| Crude & Condensates | barrels per day | `bpd` | Industry standard for oil flow rates |
|
|
128
|
+
| Clean Petroleum Products | barrels per day | `bpd` | Industry standard for oil flow rates |
|
|
129
|
+
| Dirty Petroleum Products | barrels per day | `bpd` | Industry standard for oil flow rates |
|
|
130
|
+
| LNG | tonnes | `t` | Industry standard for LNG |
|
|
131
|
+
| LPG | tonnes | `t` | Industry standard for LPG |
|
|
132
|
+
| Counting queries | count | `c` | Number of cargoes |
|
|
133
|
+
|
|
134
|
+
### All Available Units
|
|
135
|
+
|
|
136
|
+
| Unit | API Value (CTS) | API Value (CM search) | Description |
|
|
137
|
+
|---|---|---|---|
|
|
138
|
+
| barrels | `b` | `b` | Absolute oil volume |
|
|
139
|
+
| barrels per day | `bpd` | -- | Oil flow rate (most common for trends) |
|
|
140
|
+
| tonnes | `t` | `t` | Absolute weight (LNG, LPG, or oil) |
|
|
141
|
+
| tonnes per day | `tpd` | -- | Weight flow rate |
|
|
142
|
+
| cubic metres | `cbm` | `cbm` | LNG volume |
|
|
143
|
+
| count | `c` | -- | Number of cargo movements |
|
|
144
|
+
| count per day | `cpd` | -- | Cargo movement rate |
|
|
145
|
+
| million per day | `mpd` | -- | Millions per day |
|
|
146
|
+
| barrels per annum | `bpa` | -- | Annual oil rate |
|
|
147
|
+
| tonnes per annum | `tpa` | -- | Annual weight rate |
|
|
148
|
+
| million per annum | `mpa` | -- | Millions per annum |
|
|
149
|
+
|
|
150
|
+
**Note:** CargoMovements search uses `cm_unit` (only `b`, `t`, `cbm`). CargoTimeSeries uses `timeseries_unit` (full list above). VoyagesSearchEnriched uses `unit` (only `b`, `t`, `cbm`).
|
|
151
|
+
|
|
152
|
+
### Unit Rules
|
|
153
|
+
|
|
154
|
+
**NEVER** guess units if the product could use multiple -- ASK the user. Example: LNG can use tonnes (`t`) or cubic metres (`cbm`).
|
|
155
|
+
|
|
156
|
+
**ALWAYS** mention the unit choice and its rationale in the confirmation step, so the user can override.
|
|
157
|
+
|
|
158
|
+
**ALWAYS** match the unit to the commodity:
|
|
159
|
+
- Oil queries: `bpd` for trends, `b` for absolute volumes
|
|
160
|
+
- LNG queries: `t` (default), or `cbm` if user specifies volume
|
|
161
|
+
- LPG queries: `t`
|
|
162
|
+
- Counting: `c` or `cpd`
|
|
163
|
+
|
|
164
|
+
**ALWAYS** use rate units (`bpd`, `tpd`) for timeseries trends. Use absolute units (`b`, `t`) only for snapshot/total queries.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Confirmation Step: Date, Unit & Frequency
|
|
169
|
+
|
|
170
|
+
Every data query confirmation must include:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Time: Jan 1 2025, 00:00:00 -> Dec 31 2025, 23:59:59 (calendar year 2025)
|
|
174
|
+
Unit: bpd (barrels per day -- default for oil)
|
|
175
|
+
Frequency: monthly
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If the user did not specify frequency, the confirmation should say:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
Frequency: Please specify -- daily / weekly / monthly?
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
If the unit choice is non-obvious, explain why:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
Unit: tonnes (default for LNG -- would you prefer cubic metres?)
|
|
188
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Adding a New Vortexa Endpoint
|
|
2
|
+
|
|
3
|
+
> Developer guide for extending the skills package with new API endpoints.
|
|
4
|
+
> Each new endpoint requires exactly 3 files that compose shared infrastructure.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## The 3-File Pattern
|
|
9
|
+
|
|
10
|
+
| File | Location | Purpose |
|
|
11
|
+
|------|----------|---------|
|
|
12
|
+
| Context doc | `context/<endpoint>.md` | API reference: parameters, filters, columns, code examples |
|
|
13
|
+
| Python module | `lib/<module>.py` | SDK wrapper functions: query, parse, transform |
|
|
14
|
+
| Skill file | `commands/vortexa/<skill>.md` | Slash command: NL parsing, confirmation, code generation |
|
|
15
|
+
|
|
16
|
+
Everything else -- entity resolution, date parsing, guardrails, visualization, setup checks -- is shared infrastructure already built.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Shared Infrastructure (Already Built)
|
|
21
|
+
|
|
22
|
+
These are composed by all endpoints -- no duplication needed:
|
|
23
|
+
|
|
24
|
+
| Component | Path | What It Provides |
|
|
25
|
+
|-----------|------|-----------------|
|
|
26
|
+
| Guardrails | `context/guardrails.md` | NEVER/ALWAYS rules, activity filter table, confirmation template |
|
|
27
|
+
| Entity resolution | `context/entity-resolution.md` | Name-to-ID workflow, disambiguation protocol |
|
|
28
|
+
| Date & units | `context/date-units.md` | Date parsing, unit defaults, frequency guidance |
|
|
29
|
+
| Entity resolver | `lib/entities.py` | `resolve_geography()`, `resolve_product()`, `EntityCache` |
|
|
30
|
+
| API auth | `lib/api.py` | `load_api_key()`, environment validation |
|
|
31
|
+
| Utilities | `lib/utils.py` | `_to_dt()`, `_cols()`, `top_n_with_other()` |
|
|
32
|
+
| Visualization | `lib/visualization.py` | Vortexa theme, chart functions |
|
|
33
|
+
| Setup check | `commands/vortexa/_check-setup.md` | Pre-execution environment validation |
|
|
34
|
+
| Skill template | `commands/vortexa/_skill-template.md` | YAML frontmatter and process structure reference |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Step-by-Step: Adding an Endpoint
|
|
39
|
+
|
|
40
|
+
### Step 1: Context Doc (`context/<endpoint>.md`)
|
|
41
|
+
|
|
42
|
+
Create the API reference document. Follow the structure of `context/cargo-movements.md`:
|
|
43
|
+
|
|
44
|
+
1. **Header**: Endpoint name, one-line description, SDK class name
|
|
45
|
+
2. **When to Use**: Decision table -- when this endpoint vs others, with signal words
|
|
46
|
+
3. **Parameters**: Full parameter table with name, type, default, description
|
|
47
|
+
4. **Filter Options**: All filter parameters with valid values
|
|
48
|
+
5. **Response Fields**: Column/field names, types, descriptions
|
|
49
|
+
6. **Code Examples**: Working SDK code snippets for common queries
|
|
50
|
+
7. **Cross-References**: Links to guardrails.md, entity-resolution.md, date-units.md
|
|
51
|
+
|
|
52
|
+
Target: 400-700 lines. Every parameter documented. Include negative guidance (NEVER do X).
|
|
53
|
+
|
|
54
|
+
### Step 2: Python Module (`lib/<module>.py`)
|
|
55
|
+
|
|
56
|
+
Create SDK wrapper functions. Follow the structure of `lib/movements.py`:
|
|
57
|
+
|
|
58
|
+
1. **Lazy imports**: `from vortexasdk import X` inside functions, not at module level
|
|
59
|
+
2. **Internal imports**: `from lib.entities import resolve_geography, resolve_product`
|
|
60
|
+
3. **Function signature**: Accept human-readable parameters, return DataFrames
|
|
61
|
+
4. **Column renaming**: Apply `_cols()` from lib/utils.py for human-readable output
|
|
62
|
+
5. **Error handling**: Catch SDK exceptions, return helpful messages
|
|
63
|
+
|
|
64
|
+
Minimum functions:
|
|
65
|
+
- `<endpoint>_timeseries(...)` -- time-series query with filters
|
|
66
|
+
- `<endpoint>_search(...)` -- individual record search
|
|
67
|
+
- Column rename map as module-level dict
|
|
68
|
+
|
|
69
|
+
### Step 3: Skill File (`commands/vortexa/<skill>.md`)
|
|
70
|
+
|
|
71
|
+
Create the slash command. Follow the structure of `commands/vortexa/cargo-flows.md`:
|
|
72
|
+
|
|
73
|
+
1. **YAML frontmatter**: name, description, argument-hint, allowed-tools
|
|
74
|
+
2. **Objective**: What this skill does (1-3 sentences)
|
|
75
|
+
3. **Execution context**: Pre-loaded vs on-demand context docs
|
|
76
|
+
4. **Setup check**: Include `@commands/vortexa/_check-setup.md`
|
|
77
|
+
5. **Process steps**:
|
|
78
|
+
- Read endpoint context doc
|
|
79
|
+
- Parse user query (extract entities, dates, filters)
|
|
80
|
+
- Check for missing parameters (ask if needed)
|
|
81
|
+
- Resolve entity IDs
|
|
82
|
+
- Confirm before executing (MANDATORY)
|
|
83
|
+
- Generate and execute Python code artifact
|
|
84
|
+
- Present results with methodology footnote
|
|
85
|
+
- Offer CSV export and summary
|
|
86
|
+
|
|
87
|
+
Target: 180-250 lines.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Worked Example: Freight Outlook (Hypothetical)
|
|
92
|
+
|
|
93
|
+
To add Freight Outlook as a new endpoint:
|
|
94
|
+
|
|
95
|
+
### File 1: `context/freight-outlook.md`
|
|
96
|
+
|
|
97
|
+
Document all FFA routes (TC1-TC17, TD1-TD25), pricing parameters, rate types (flat rate, worldscale, $/tonne), time-series options, and settlement conventions. Include a decision table for when to use Freight Outlook vs CargoTimeSeries for cost analysis. Cross-reference guardrails.md for confirmation template and date-units.md for time range parsing.
|
|
98
|
+
|
|
99
|
+
### File 2: `lib/freight.py`
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
"""Freight outlook queries and rate helpers."""
|
|
103
|
+
|
|
104
|
+
import pandas as pd
|
|
105
|
+
|
|
106
|
+
# column rename map
|
|
107
|
+
freight_cols = {
|
|
108
|
+
"key": "route",
|
|
109
|
+
"value": "rate",
|
|
110
|
+
"count": "fixtures",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def freight_timeseries(route, time_min, time_max, rate_type="flat_rate", frequency="day"):
|
|
114
|
+
"""Query freight rate timeseries for a given route."""
|
|
115
|
+
from vortexasdk import FreightPricing
|
|
116
|
+
from lib.utils import _to_dt, _cols
|
|
117
|
+
|
|
118
|
+
result = FreightPricing().search(
|
|
119
|
+
route=route,
|
|
120
|
+
filter_time_min=_to_dt(time_min),
|
|
121
|
+
filter_time_max=_to_dt(time_max),
|
|
122
|
+
rate_type=rate_type,
|
|
123
|
+
breakdown_frequency=frequency,
|
|
124
|
+
).to_df()
|
|
125
|
+
|
|
126
|
+
return _cols(result, freight_cols)
|
|
127
|
+
|
|
128
|
+
def freight_search(route, time_min, time_max, size=500):
|
|
129
|
+
"""Search individual freight fixtures for a route."""
|
|
130
|
+
from vortexasdk import FreightPricing
|
|
131
|
+
from lib.utils import _to_dt, _cols
|
|
132
|
+
|
|
133
|
+
result = FreightPricing().search(
|
|
134
|
+
route=route,
|
|
135
|
+
filter_time_min=_to_dt(time_min),
|
|
136
|
+
filter_time_max=_to_dt(time_max),
|
|
137
|
+
size=size,
|
|
138
|
+
).to_df()
|
|
139
|
+
|
|
140
|
+
return _cols(result, freight_cols)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### File 3: `commands/vortexa/freight.md`
|
|
144
|
+
|
|
145
|
+
Register as `/vortexa:freight`. Parse route names from natural language ("TD3C", "AG to Japan VLCC"), resolve to Vortexa route IDs, confirm rate type and time range, generate Python code artifact using `lib/freight.py`, present rate chart with Vortexa theme.
|
|
146
|
+
|
|
147
|
+
### Shared infrastructure reused automatically:
|
|
148
|
+
|
|
149
|
+
- Entity resolution for vessel classes and geographies
|
|
150
|
+
- Date parsing for time ranges
|
|
151
|
+
- Vortexa theme for rate charts
|
|
152
|
+
- Setup check for environment validation
|
|
153
|
+
- Guardrails confirmation template
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Checklist for New Endpoint
|
|
158
|
+
|
|
159
|
+
Before submitting, verify all items:
|
|
160
|
+
|
|
161
|
+
- [ ] Context doc covers all parameters and response fields
|
|
162
|
+
- [ ] Context doc includes NEVER/ALWAYS negative guidance
|
|
163
|
+
- [ ] Context doc has "When to Use" decision table with signal words
|
|
164
|
+
- [ ] Context doc cross-references guardrails.md, entity-resolution.md, date-units.md
|
|
165
|
+
- [ ] Python module uses lazy imports (no top-level vortexasdk)
|
|
166
|
+
- [ ] Python module uses `_cols()` for column renaming
|
|
167
|
+
- [ ] Python module has both timeseries and search functions
|
|
168
|
+
- [ ] Python module has module-level column rename dict
|
|
169
|
+
- [ ] Skill file includes setup check via `@commands/vortexa/_check-setup.md`
|
|
170
|
+
- [ ] Skill file has mandatory confirmation step before execution
|
|
171
|
+
- [ ] Skill file includes methodology footnote in results
|
|
172
|
+
- [ ] Skill file offers CSV export after presenting results
|
|
173
|
+
- [ ] All entity resolution goes through `lib/entities.py`
|
|
174
|
+
- [ ] Generated code uses `sys.path.insert(0, "lib")` pattern
|
|
175
|
+
- [ ] npm `files` allowlist includes the new files' directories (already covered if using `context/`, `lib/`, `commands/`)
|
|
176
|
+
- [ ] CLAUDE.md "Available Skills" section updated with the new skill
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Entity Resolution
|
|
2
|
+
|
|
3
|
+
> Step-by-step workflow for resolving human-readable names to Vortexa 64-char hex IDs.
|
|
4
|
+
> Loaded automatically for every /vortexa: skill.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Resolution Workflow
|
|
9
|
+
|
|
10
|
+
Every Vortexa API filter requires hex IDs, not names. Follow this process for every entity:
|
|
11
|
+
|
|
12
|
+
1. **Identify entity type** -- geography, product, vessel, or corporate
|
|
13
|
+
2. **Call the correct SDK search method** with appropriate filters (especially `filter_layer` for geographies and products)
|
|
14
|
+
3. **Handle results:**
|
|
15
|
+
- 1 match = use it
|
|
16
|
+
- Multiple matches = disambiguate (present to user)
|
|
17
|
+
- 0 matches = suggest alternatives (broaden search, check spelling, try partial name)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Entity Types & SDK Methods
|
|
22
|
+
|
|
23
|
+
| Entity Type | SDK Class | Search Method | Key Filter | Size Default |
|
|
24
|
+
|---|---|---|---|---|
|
|
25
|
+
| Geography | `Geographies` | `.search(term=..., filter_layer=...)` | `filter_layer` (critical) | 1 -- set higher |
|
|
26
|
+
| Product | `Products` | `.search(term=..., filter_layer=...)` | `filter_layer` | 1 -- set higher |
|
|
27
|
+
| Vessel | `Vessels` | `.search(term=..., vessel_classes=...)` | `vessel_classes` | 1 -- set higher |
|
|
28
|
+
| Corporation | `Corporations` | `.search(term=...)` | (none in SDK) | 1 -- set higher |
|
|
29
|
+
|
|
30
|
+
**ALWAYS** set `size` to at least 10 for resolution searches. Default `size=1` may return the wrong match.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Geography Resolution
|
|
35
|
+
|
|
36
|
+
Geographies have a deep hierarchy. The `filter_layer` parameter is mandatory for accurate resolution.
|
|
37
|
+
|
|
38
|
+
### Hierarchy (widest to most specific)
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
wider_shipping_region > shipping_region_v2 > region > country > port > terminal
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Resolution Table
|
|
45
|
+
|
|
46
|
+
| User Says | filter_layer | Example Vortexa Name | Notes |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| "China", "US", "Saudi Arabia" | `country` | China, United States, Saudi Arabia | Country names |
|
|
49
|
+
| "Rotterdam", "Fujairah", "Ras Tanura" | `port` | Rotterdam [NL], Fujairah, Ras Tanura | Port names |
|
|
50
|
+
| "Middle East", "Asia", "Europe" | `region` | Middle East/North Africa, Asia, Europe | Broad regions |
|
|
51
|
+
| "AG", "Arabian Gulf", "Persian Gulf" | `shipping_region_v2` | Arabian/Persian Gulf | Shipping regions |
|
|
52
|
+
| "USG", "US Gulf Coast" | `shipping_region_v2` | US Gulf Coast | Shipping regions |
|
|
53
|
+
| "WAF", "West Africa" | `shipping_region_v2` | West Africa | Shipping regions |
|
|
54
|
+
| "ARA" | `port` (multiple) | Amsterdam, Rotterdam, Antwerp | Resolve each port separately |
|
|
55
|
+
| "PADD I", "East Coast" | `shipping_region_v2` | US Atlantic Coast | US regional |
|
|
56
|
+
| "Suez Canal", "Panama Canal" | `waypoint` | Suez Canal, Panama Canal | Chokepoints |
|
|
57
|
+
|
|
58
|
+
### Code Pattern
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from vortexasdk import Geographies
|
|
62
|
+
|
|
63
|
+
# ALWAYS specify filter_layer
|
|
64
|
+
result = Geographies().search(
|
|
65
|
+
term="Saudi Arabia",
|
|
66
|
+
filter_layer="country",
|
|
67
|
+
exact_term_match=True
|
|
68
|
+
).to_df()
|
|
69
|
+
|
|
70
|
+
saudi_id = result.iloc[0]["id"] # 64-char hex
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**NEVER** search geographies without `filter_layer`. "China" without a layer returns ports, regions, and the country.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Product Resolution
|
|
78
|
+
|
|
79
|
+
Products have a 4-level hierarchy. Match the `filter_layer` to the user's specificity.
|
|
80
|
+
|
|
81
|
+
### Hierarchy
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
group > group_product > category > grade
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Resolution Table
|
|
88
|
+
|
|
89
|
+
| User Says | filter_layer | Vortexa Name | Notes |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| "crude", "crude oil" | `group` | Crude & Condensates | Broadest oil group |
|
|
92
|
+
| "clean products", "CPP" | `group` | Clean Petroleum Products | |
|
|
93
|
+
| "dirty products", "DPP" | `group` | Dirty Petroleum Products | |
|
|
94
|
+
| "LNG" | `group` | LNG | |
|
|
95
|
+
| "LPG" | `group` | LPG | |
|
|
96
|
+
| "fuel oil" | `group_product` | Fuel Oil | Under Dirty Products |
|
|
97
|
+
| "naphtha" | `group_product` | Naphtha | Under Clean Products |
|
|
98
|
+
| "gasoline" | `group_product` | Gasoline / Mogas | Under Clean Products |
|
|
99
|
+
| "gasoil", "diesel" | `group_product` | Gasoil/Diesel | Under Clean Products |
|
|
100
|
+
| "condensate" | `group_product` | Condensate | Under Crude & Condensates |
|
|
101
|
+
| "Arab Light", "Bonny Light" | `grade` | Arab Light, Bonny Light | Most specific |
|
|
102
|
+
|
|
103
|
+
### Code Pattern
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from vortexasdk import Products
|
|
107
|
+
|
|
108
|
+
result = Products().search(
|
|
109
|
+
term="crude",
|
|
110
|
+
filter_layer="group"
|
|
111
|
+
).to_df()
|
|
112
|
+
|
|
113
|
+
# Verify the match -- "crude" may return multiple layers
|
|
114
|
+
crude_id = result[result["name"] == "Crude & Condensates"].iloc[0]["id"]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**ALWAYS** verify the `layer` and `name` columns in results. Fuzzy matching may return grades when you want the group.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Vessel Resolution
|
|
122
|
+
|
|
123
|
+
Vessels are searched by name, IMO number, or class. No `filter_layer` applies.
|
|
124
|
+
|
|
125
|
+
| Search By | Method | Notes |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Vessel name | `Vessels().search(term="vessel name")` | Fuzzy match; may return multiple |
|
|
128
|
+
| IMO number | `Vessels().search(ids=[9779836])` | Pass IMO as integer, not string |
|
|
129
|
+
| Vessel class | `Vessels().search(vessel_classes="oil_vlcc")` | Returns all vessels of that class |
|
|
130
|
+
| Name + class | `Vessels().search(term="ocean", vessel_classes="oil_vlcc")` | Narrows by both |
|
|
131
|
+
|
|
132
|
+
### Code Pattern
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from vortexasdk import Vessels
|
|
136
|
+
|
|
137
|
+
# By name
|
|
138
|
+
result = Vessels().search(term="Olympic Pride").to_df()
|
|
139
|
+
|
|
140
|
+
# By IMO (MUST be integer)
|
|
141
|
+
result = Vessels().search(ids=[9779836]).to_df(columns="all")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**NEVER** pass IMO numbers as strings -- the API requires integers in the `ids` or `term` array.
|
|
145
|
+
|
|
146
|
+
For vessel class filtering in data queries (CM/Voyages), use `filter_vessel_classes=["oil_vlcc"]` directly -- no ID resolution needed.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Corporate Resolution
|
|
151
|
+
|
|
152
|
+
Corporations (shipowners, charterers, traders) are searched by name only. The SDK does not support filtering by entity type.
|
|
153
|
+
|
|
154
|
+
| Search By | Method | Notes |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| Company name | `Corporations().search(term="Shell")` | Fuzzy match |
|
|
157
|
+
| Exact name | `Corporations().search(term="CHEVRON", exact_term_match=True)` | Exact match |
|
|
158
|
+
|
|
159
|
+
### Code Pattern
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from vortexasdk import Corporations
|
|
163
|
+
|
|
164
|
+
result = Corporations().search(term="BAHRI", exact_term_match=True).to_df()
|
|
165
|
+
corp_id = result.iloc[0]["id"]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Limitation:** The SDK `Corporations` class does not expose `corporateEntityType` filtering. To filter by role (effective_controller, charterer, etc.), use the REST API directly:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import requests
|
|
172
|
+
|
|
173
|
+
payload = {
|
|
174
|
+
"term": "CHEVRON",
|
|
175
|
+
"corporateEntityType": ["effective_controller"],
|
|
176
|
+
"size": 10
|
|
177
|
+
}
|
|
178
|
+
resp = requests.post(
|
|
179
|
+
"https://api.vortexa.com/v5/reference/charterers",
|
|
180
|
+
headers={"x-api-key": VORTEXA_API_KEY},
|
|
181
|
+
json=payload
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Disambiguation Protocol
|
|
188
|
+
|
|
189
|
+
When a search returns multiple matches:
|
|
190
|
+
|
|
191
|
+
1. **Present top 5 matches** to the user with: name, type/layer, and first 8 chars of ID
|
|
192
|
+
2. **Ask the user to select** the correct entity
|
|
193
|
+
3. **NEVER guess** -- ambiguous entities cause silently wrong results
|
|
194
|
+
|
|
195
|
+
### Example Disambiguation
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
Multiple matches found for "Gulf":
|
|
199
|
+
|
|
200
|
+
1. Arabian/Persian Gulf (shipping_region_v2) - ID: a3c1e9f2...
|
|
201
|
+
2. US Gulf Coast (shipping_region_v2) - ID: b7d4f8a1...
|
|
202
|
+
3. Gulf of Mexico (region) - ID: c2e5a7b3...
|
|
203
|
+
4. Gulf of Oman (shipping_region_v2) - ID: d8f1c4e6...
|
|
204
|
+
|
|
205
|
+
Which did you mean?
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## ID Format
|
|
211
|
+
|
|
212
|
+
| Format | Length | Used In | Example |
|
|
213
|
+
|---|---|---|---|
|
|
214
|
+
| Full hex ID | 64 characters | All API filter parameters | `a1b2c3d4e5f6...` (64 chars) |
|
|
215
|
+
| Truncated ID | 16 characters | `cargo_movement_id` in joins, voyage links | `a1b2c3d4e5f6a7b8` |
|
|
216
|
+
|
|
217
|
+
**ALWAYS** use full 64-character IDs for API calls. Truncated 16-char IDs appear in cross-endpoint joins (e.g., matching cargo movements to voyages) but are not valid for filter parameters.
|