kensho-kfinance 3.2.4__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/METADATA +3 -3
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/RECORD +57 -56
- kfinance/CHANGELOG.md +51 -0
- kfinance/client/batch_request_handling.py +3 -1
- kfinance/client/fetch.py +127 -54
- kfinance/client/kfinance.py +38 -39
- kfinance/client/meta_classes.py +50 -20
- kfinance/client/models/date_and_period_models.py +32 -7
- kfinance/client/models/decimal_with_unit.py +14 -2
- kfinance/client/models/response_models.py +33 -0
- kfinance/client/models/tests/test_decimal_with_unit.py +9 -0
- kfinance/client/tests/test_batch_requests.py +5 -4
- kfinance/client/tests/test_fetch.py +134 -58
- kfinance/client/tests/test_objects.py +207 -145
- kfinance/conftest.py +10 -0
- kfinance/domains/business_relationships/business_relationship_tools.py +17 -8
- kfinance/domains/business_relationships/tests/test_business_relationship_tools.py +18 -16
- kfinance/domains/capitalizations/capitalization_models.py +7 -5
- kfinance/domains/capitalizations/capitalization_tools.py +38 -20
- kfinance/domains/capitalizations/tests/test_capitalization_tools.py +66 -36
- kfinance/domains/companies/company_models.py +22 -2
- kfinance/domains/companies/company_tools.py +49 -16
- kfinance/domains/companies/tests/test_company_tools.py +27 -9
- kfinance/domains/competitors/competitor_tools.py +19 -5
- kfinance/domains/competitors/tests/test_competitor_tools.py +22 -19
- kfinance/domains/cusip_and_isin/cusip_and_isin_tools.py +29 -8
- kfinance/domains/cusip_and_isin/tests/test_cusip_and_isin_tools.py +13 -8
- kfinance/domains/earnings/earning_tools.py +73 -29
- kfinance/domains/earnings/tests/test_earnings_tools.py +52 -43
- kfinance/domains/line_items/line_item_models.py +372 -16
- kfinance/domains/line_items/line_item_tools.py +198 -46
- kfinance/domains/line_items/tests/test_line_item_tools.py +305 -39
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_models.py +46 -2
- kfinance/domains/mergers_and_acquisitions/merger_and_acquisition_tools.py +55 -74
- kfinance/domains/mergers_and_acquisitions/tests/test_merger_and_acquisition_tools.py +61 -59
- kfinance/domains/prices/price_models.py +7 -6
- kfinance/domains/prices/price_tools.py +24 -16
- kfinance/domains/prices/tests/test_price_tools.py +47 -39
- kfinance/domains/segments/segment_models.py +17 -3
- kfinance/domains/segments/segment_tools.py +102 -42
- kfinance/domains/segments/tests/test_segment_tools.py +166 -37
- kfinance/domains/statements/statement_models.py +17 -3
- kfinance/domains/statements/statement_tools.py +130 -46
- kfinance/domains/statements/tests/test_statement_tools.py +251 -49
- kfinance/integrations/local_mcp/kfinance_mcp.py +1 -1
- kfinance/integrations/tests/test_example_notebook.py +57 -16
- kfinance/integrations/tool_calling/all_tools.py +5 -1
- kfinance/integrations/tool_calling/static_tools/get_n_quarters_ago.py +5 -0
- kfinance/integrations/tool_calling/static_tools/tests/test_get_lastest.py +13 -10
- kfinance/integrations/tool_calling/static_tools/tests/test_get_n_quarters_ago.py +2 -1
- kfinance/integrations/tool_calling/tests/test_tool_calling_models.py +15 -4
- kfinance/integrations/tool_calling/tool_calling_models.py +18 -6
- kfinance/version.py +2 -2
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/WHEEL +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/AUTHORS.md +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/licenses/LICENSE +0 -0
- {kensho_kfinance-3.2.4.dist-info → kensho_kfinance-4.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
from difflib import SequenceMatcher
|
|
1
2
|
from textwrap import dedent
|
|
2
3
|
from typing import Literal, Type
|
|
3
4
|
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
5
6
|
|
|
6
|
-
from kfinance.client.
|
|
7
|
-
from kfinance.client.models.date_and_period_models import PeriodType
|
|
7
|
+
from kfinance.client.models.date_and_period_models import NumPeriods, NumPeriodsBack, PeriodType
|
|
8
8
|
from kfinance.client.permission_models import Permission
|
|
9
9
|
from kfinance.domains.line_items.line_item_models import (
|
|
10
10
|
LINE_ITEM_NAMES_AND_ALIASES,
|
|
11
|
-
|
|
11
|
+
LINE_ITEM_TO_DESCRIPTIONS_MAP,
|
|
12
|
+
CalendarType,
|
|
13
|
+
LineItemResp,
|
|
14
|
+
LineItemScore,
|
|
12
15
|
)
|
|
13
16
|
from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
14
17
|
KfinanceTool,
|
|
@@ -18,6 +21,75 @@ from kfinance.integrations.tool_calling.tool_calling_models import (
|
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def _find_similar_line_items(
|
|
25
|
+
invalid_item: str, descriptors: dict[str, str], max_suggestions: int = 8
|
|
26
|
+
) -> list[LineItemScore]:
|
|
27
|
+
"""Find similar line items using keyword matching and string similarity.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
invalid_item: The invalid line item provided by the user
|
|
31
|
+
descriptors: Dictionary mapping line item names to descriptions
|
|
32
|
+
max_suggestions: Maximum number of suggestions to return
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List of LineItemScore objects for the best matches
|
|
36
|
+
"""
|
|
37
|
+
if not descriptors:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
invalid_lower = invalid_item.lower()
|
|
41
|
+
scores: list[LineItemScore] = []
|
|
42
|
+
|
|
43
|
+
for line_item, description in descriptors.items():
|
|
44
|
+
# Calculate similarity scores
|
|
45
|
+
name_similarity = SequenceMatcher(None, invalid_lower, line_item.lower()).ratio()
|
|
46
|
+
|
|
47
|
+
# Check for keyword matches in the line item name
|
|
48
|
+
invalid_words = set(invalid_lower.replace("_", " ").split())
|
|
49
|
+
item_words = set(line_item.lower().replace("_", " ").split())
|
|
50
|
+
keyword_match_score = len(invalid_words.intersection(item_words)) / max(
|
|
51
|
+
len(invalid_words), 1
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Check for keyword matches in description
|
|
55
|
+
description_words = set(description.lower().split())
|
|
56
|
+
description_match_score = len(invalid_words.intersection(description_words)) / max(
|
|
57
|
+
len(invalid_words), 1
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Combined score (weighted)
|
|
61
|
+
total_score = (
|
|
62
|
+
name_similarity * 0.5 # Direct name similarity
|
|
63
|
+
+ keyword_match_score * 0.3 # Keyword matches in name
|
|
64
|
+
+ description_match_score * 0.2 # Keyword matches in description
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
scores.append(LineItemScore(name=line_item, description=description, score=total_score))
|
|
68
|
+
|
|
69
|
+
# Sort by score (descending) and return top matches
|
|
70
|
+
scores.sort(reverse=True, key=lambda x: x.score)
|
|
71
|
+
return [item for item in scores[:max_suggestions] if item.score > 0.1]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _smart_line_item_validator(v: str) -> str:
|
|
75
|
+
"""Custom validator that provides intelligent suggestions for invalid line items."""
|
|
76
|
+
if v not in LINE_ITEM_NAMES_AND_ALIASES:
|
|
77
|
+
# Find similar items using pre-computed descriptors
|
|
78
|
+
suggestions = _find_similar_line_items(v, LINE_ITEM_TO_DESCRIPTIONS_MAP)
|
|
79
|
+
|
|
80
|
+
if suggestions:
|
|
81
|
+
suggestion_text = "\n\nDid you mean one of these?\n"
|
|
82
|
+
for item in suggestions:
|
|
83
|
+
suggestion_text += f" • '{item.name}': {item.description}\n"
|
|
84
|
+
|
|
85
|
+
error_msg = f"Invalid line_item '{v}'.{suggestion_text}"
|
|
86
|
+
else:
|
|
87
|
+
error_msg = f"Invalid line_item '{v}'. Please refer to the tool documentation for valid options."
|
|
88
|
+
|
|
89
|
+
raise ValueError(error_msg)
|
|
90
|
+
return v
|
|
91
|
+
|
|
92
|
+
|
|
21
93
|
class GetFinancialLineItemFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
22
94
|
# Note: mypy will not enforce this literal because of the type: ignore.
|
|
23
95
|
# But pydantic still uses the literal to check for allowed values and only includes
|
|
@@ -25,15 +97,47 @@ class GetFinancialLineItemFromIdentifiersArgs(ToolArgsWithIdentifiers):
|
|
|
25
97
|
line_item: Literal[tuple(LINE_ITEM_NAMES_AND_ALIASES)] = Field( # type: ignore[valid-type]
|
|
26
98
|
description="The type of financial line_item requested"
|
|
27
99
|
)
|
|
28
|
-
period_type: PeriodType | None = Field(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
100
|
+
period_type: PeriodType | None = Field(
|
|
101
|
+
default=None, description="The period type (annual or quarterly)"
|
|
102
|
+
)
|
|
103
|
+
start_year: int | None = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
description="The starting year for the data range. Use null for the most recent data.",
|
|
106
|
+
)
|
|
107
|
+
end_year: int | None = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description="The ending year for the data range. Use null for the most recent data.",
|
|
110
|
+
)
|
|
111
|
+
start_quarter: ValidQuarter | None = Field(
|
|
112
|
+
default=None, description="Starting quarter (1-4). Only used when period_type is quarterly."
|
|
113
|
+
)
|
|
114
|
+
end_quarter: ValidQuarter | None = Field(
|
|
115
|
+
default=None, description="Ending quarter (1-4). Only used when period_type is quarterly."
|
|
116
|
+
)
|
|
117
|
+
calendar_type: CalendarType | None = Field(
|
|
118
|
+
default=None, description="Fiscal year or calendar year"
|
|
119
|
+
)
|
|
120
|
+
num_periods: NumPeriods | None = Field(
|
|
121
|
+
default=None, description="The number of periods to retrieve data for (1-99)"
|
|
122
|
+
)
|
|
123
|
+
num_periods_back: NumPeriodsBack | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="The end period of the data range expressed as number of periods back relative to the present period (0-99)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@model_validator(mode="before")
|
|
129
|
+
@classmethod
|
|
130
|
+
def validate_line_item_with_suggestions(cls, values: dict) -> dict:
|
|
131
|
+
"""Custom validator that provides intelligent suggestions for invalid line items."""
|
|
132
|
+
if isinstance(values, dict) and "line_item" in values:
|
|
133
|
+
line_item = values["line_item"]
|
|
134
|
+
# Use the helper function to validate and provide suggestions
|
|
135
|
+
_smart_line_item_validator(line_item)
|
|
136
|
+
return values
|
|
33
137
|
|
|
34
138
|
|
|
35
139
|
class GetFinancialLineItemFromIdentifiersResp(ToolRespWithErrors):
|
|
36
|
-
results: dict[str,
|
|
140
|
+
results: dict[str, LineItemResp] # identifier -> response
|
|
37
141
|
|
|
38
142
|
|
|
39
143
|
class GetFinancialLineItemFromIdentifiers(KfinanceTool):
|
|
@@ -42,11 +146,27 @@ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
|
|
|
42
146
|
Get the financial line item associated with a list of identifiers.
|
|
43
147
|
|
|
44
148
|
- When possible, pass multiple identifiers in a single call rather than making multiple calls.
|
|
45
|
-
- To fetch the most recent value
|
|
149
|
+
- To fetch the most recent value, leave start_year, start_quarter, end_year, end_quarter, num_periods, and num_periods_back as null.
|
|
150
|
+
- The tool accepts an optional calendar_type argument, which can either be 'calendar' or 'fiscal'. If 'calendar' is chosen, then start_year and end_year will filter on calendar year, and the output returned will be in calendar years. If 'fiscal' is chosen (which is the default), then start_year and end_year will filter on fiscal year, and the output returned will be in fiscal years.
|
|
151
|
+
- All aliases for a line item return identical data (e.g., 'revenue', 'normal_revenue', and 'regular_revenue' return the same data).
|
|
152
|
+
- Line item names are case-insensitive and use underscores (e.g., 'total_revenue' not 'Total Revenue').
|
|
153
|
+
- To filter by time, use either absolute (start_year, end_year, start_quarter, end_quarter) for specific dates like "in 2023" or "Q2 2021", OR relative (num_periods, num_periods_back) for phrases like "last 3 quarters" or "past five years"—but not both.
|
|
46
154
|
|
|
47
|
-
|
|
155
|
+
Examples:
|
|
48
156
|
Query: "What are the revenues of Lowe's and Home Depot?"
|
|
49
|
-
Function: get_financial_line_item_from_identifiers(line_item="revenue",
|
|
157
|
+
Function: get_financial_line_item_from_identifiers(line_item="revenue", identifiers=["Lowe's", "Home Depot"])
|
|
158
|
+
|
|
159
|
+
Query: "Get MSFT and AAPL revenue"
|
|
160
|
+
Function: get_financial_line_item_from_identifiers(line_item="revenue", identifiers=["MSFT", "AAPL"])
|
|
161
|
+
|
|
162
|
+
Query: "General Eletrics's ebt excluding unusual items for 2023"
|
|
163
|
+
Function: get_financial_line_item_from_identifiers(line_item="ebt_excluding_unusual_items", identifiers=["General Eletric"], period_type="annual", start_year=2023, end_year=2023)
|
|
164
|
+
|
|
165
|
+
Query: "What is the most recent three quarters' but one ppe for Exxon and Hasbro?"
|
|
166
|
+
Function: get_financial_line_item_from_identifiers(line_item="ppe", period_type="quarterly", num_periods=3, num_periods_back=1, identifiers=["Exxon", "Hasbro"])
|
|
167
|
+
|
|
168
|
+
Query: "What are the ytd operating income values for Hilton for the calendar year 2022?"
|
|
169
|
+
Function: get_financial_line_item_from_identifiers(line_item="operating_income", period_type="ytd", calendar_type="calendar", start_year=2022, end_year=2022, identifiers=["Hilton"])
|
|
50
170
|
""").strip()
|
|
51
171
|
args_schema: Type[BaseModel] = GetFinancialLineItemFromIdentifiersArgs
|
|
52
172
|
accepted_permissions: set[Permission] | None = {
|
|
@@ -63,41 +183,72 @@ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
|
|
|
63
183
|
end_year: int | None = None,
|
|
64
184
|
start_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
65
185
|
end_quarter: Literal[1, 2, 3, 4] | None = None,
|
|
66
|
-
|
|
186
|
+
calendar_type: CalendarType | None = None,
|
|
187
|
+
num_periods: int | None = None,
|
|
188
|
+
num_periods_back: int | None = None,
|
|
189
|
+
) -> GetFinancialLineItemFromIdentifiersResp:
|
|
67
190
|
"""Sample response:
|
|
68
191
|
|
|
69
192
|
{
|
|
70
193
|
'SPGI': {
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
|
|
194
|
+
'currency': 'USD',
|
|
195
|
+
'periods': {
|
|
196
|
+
'FY2022': {
|
|
197
|
+
'period_end_date': '2022-12-31',
|
|
198
|
+
'num_months': 12,
|
|
199
|
+
'line_item': {
|
|
200
|
+
'name': 'Revenue',
|
|
201
|
+
'value': 11181000000.0,
|
|
202
|
+
'sources': [
|
|
203
|
+
{
|
|
204
|
+
'type': 'doc-viewer line item',
|
|
205
|
+
'url': 'https://www.capitaliq.spglobal.com/...'
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
'FY2023': {
|
|
211
|
+
'period_end_date': '2023-12-31',
|
|
212
|
+
'num_months': 12,
|
|
213
|
+
'line_item': {
|
|
214
|
+
'name': 'Revenue',
|
|
215
|
+
'value': 12497000000.0,
|
|
216
|
+
'sources': [
|
|
217
|
+
{
|
|
218
|
+
'type': 'doc-viewer line item',
|
|
219
|
+
'url': 'https://www.capitaliq.spglobal.com/...'
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
74
225
|
}
|
|
75
226
|
}
|
|
227
|
+
|
|
76
228
|
"""
|
|
77
229
|
api_client = self.kfinance_client.kfinance_api_client
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
for identifier, id_triple in id_triple_resp.identifiers_to_id_triples.items()
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
line_item_responses: dict[str, LineItemResponse] = process_tasks_in_thread_pool_executor(
|
|
98
|
-
api_client=api_client, tasks=tasks
|
|
230
|
+
|
|
231
|
+
# First resolve identifiers to company IDs
|
|
232
|
+
ids_response = api_client.unified_fetch_id_triples(identifiers)
|
|
233
|
+
|
|
234
|
+
response = api_client.fetch_line_item(
|
|
235
|
+
company_ids=ids_response.company_ids,
|
|
236
|
+
line_item=line_item,
|
|
237
|
+
period_type=period_type,
|
|
238
|
+
start_year=start_year,
|
|
239
|
+
end_year=end_year,
|
|
240
|
+
start_quarter=start_quarter,
|
|
241
|
+
end_quarter=end_quarter,
|
|
242
|
+
calendar_type=calendar_type,
|
|
243
|
+
num_periods=num_periods,
|
|
244
|
+
num_periods_back=num_periods_back,
|
|
99
245
|
)
|
|
100
246
|
|
|
247
|
+
identifier_to_results = {}
|
|
248
|
+
for company_id_str, line_item_resp in response.results.items():
|
|
249
|
+
original_identifier = ids_response.get_identifier_from_company_id(int(company_id_str))
|
|
250
|
+
identifier_to_results[original_identifier] = line_item_resp
|
|
251
|
+
|
|
101
252
|
# If no date and multiple companies, only return the most recent value.
|
|
102
253
|
# By default, we return 5 years of data, which can be too much when
|
|
103
254
|
# returning data for many companies.
|
|
@@ -106,14 +257,15 @@ class GetFinancialLineItemFromIdentifiers(KfinanceTool):
|
|
|
106
257
|
and end_year is None
|
|
107
258
|
and start_quarter is None
|
|
108
259
|
and end_quarter is None
|
|
109
|
-
and
|
|
260
|
+
and num_periods is None
|
|
261
|
+
and num_periods_back is None
|
|
262
|
+
and len(identifier_to_results) > 1
|
|
110
263
|
):
|
|
111
|
-
for line_item_response in
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
264
|
+
for line_item_response in identifier_to_results.values():
|
|
265
|
+
line_item_response.remove_all_periods_other_than_the_most_recent_one()
|
|
266
|
+
|
|
267
|
+
all_errors = list(ids_response.errors.values()) + list(response.errors.values())
|
|
115
268
|
|
|
116
|
-
|
|
117
|
-
results=
|
|
269
|
+
return GetFinancialLineItemFromIdentifiersResp(
|
|
270
|
+
results=identifier_to_results, errors=all_errors
|
|
118
271
|
)
|
|
119
|
-
return output_model.model_dump(mode="json")
|