pytrilogy 0.3.149__cp313-cp313-win_amd64.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.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
- pytrilogy-0.3.149.dist-info/METADATA +555 -0
- pytrilogy-0.3.149.dist-info/RECORD +207 -0
- pytrilogy-0.3.149.dist-info/WHEEL +4 -0
- pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.149.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +27 -0
- trilogy/ai/README.md +10 -0
- trilogy/ai/__init__.py +19 -0
- trilogy/ai/constants.py +92 -0
- trilogy/ai/conversation.py +107 -0
- trilogy/ai/enums.py +7 -0
- trilogy/ai/execute.py +50 -0
- trilogy/ai/models.py +34 -0
- trilogy/ai/prompts.py +100 -0
- trilogy/ai/providers/__init__.py +0 -0
- trilogy/ai/providers/anthropic.py +106 -0
- trilogy/ai/providers/base.py +24 -0
- trilogy/ai/providers/google.py +146 -0
- trilogy/ai/providers/openai.py +89 -0
- trilogy/ai/providers/utils.py +68 -0
- trilogy/authoring/README.md +3 -0
- trilogy/authoring/__init__.py +148 -0
- trilogy/constants.py +119 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +454 -0
- trilogy/core/env_processor.py +239 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1240 -0
- trilogy/core/graph_models.py +142 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2670 -0
- trilogy/core/models/build.py +2603 -0
- trilogy/core/models/build_environment.py +165 -0
- trilogy/core/models/core.py +506 -0
- trilogy/core/models/datasource.py +436 -0
- trilogy/core/models/environment.py +756 -0
- trilogy/core/models/execute.py +1213 -0
- trilogy/core/optimization.py +251 -0
- trilogy/core/optimizations/__init__.py +12 -0
- trilogy/core/optimizations/base_optimization.py +17 -0
- trilogy/core/optimizations/hide_unused_concept.py +47 -0
- trilogy/core/optimizations/inline_datasource.py +102 -0
- trilogy/core/optimizations/predicate_pushdown.py +245 -0
- trilogy/core/processing/README.md +94 -0
- trilogy/core/processing/READMEv2.md +121 -0
- trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +508 -0
- trilogy/core/processing/constants.py +15 -0
- trilogy/core/processing/discovery_node_factory.py +451 -0
- trilogy/core/processing/discovery_utility.py +548 -0
- trilogy/core/processing/discovery_validation.py +167 -0
- trilogy/core/processing/graph_utils.py +43 -0
- trilogy/core/processing/node_generators/README.md +9 -0
- trilogy/core/processing/node_generators/__init__.py +31 -0
- trilogy/core/processing/node_generators/basic_node.py +160 -0
- trilogy/core/processing/node_generators/common.py +270 -0
- trilogy/core/processing/node_generators/constant_node.py +38 -0
- trilogy/core/processing/node_generators/filter_node.py +315 -0
- trilogy/core/processing/node_generators/group_node.py +213 -0
- trilogy/core/processing/node_generators/group_to_node.py +117 -0
- trilogy/core/processing/node_generators/multiselect_node.py +207 -0
- trilogy/core/processing/node_generators/node_merge_node.py +695 -0
- trilogy/core/processing/node_generators/recursive_node.py +88 -0
- trilogy/core/processing/node_generators/rowset_node.py +165 -0
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
- trilogy/core/processing/node_generators/select_merge_node.py +846 -0
- trilogy/core/processing/node_generators/select_node.py +95 -0
- trilogy/core/processing/node_generators/synonym_node.py +98 -0
- trilogy/core/processing/node_generators/union_node.py +91 -0
- trilogy/core/processing/node_generators/unnest_node.py +182 -0
- trilogy/core/processing/node_generators/window_node.py +201 -0
- trilogy/core/processing/nodes/README.md +28 -0
- trilogy/core/processing/nodes/__init__.py +179 -0
- trilogy/core/processing/nodes/base_node.py +522 -0
- trilogy/core/processing/nodes/filter_node.py +75 -0
- trilogy/core/processing/nodes/group_node.py +194 -0
- trilogy/core/processing/nodes/merge_node.py +420 -0
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/processing/nodes/select_node_v2.py +242 -0
- trilogy/core/processing/nodes/union_node.py +53 -0
- trilogy/core/processing/nodes/unnest_node.py +62 -0
- trilogy/core/processing/nodes/window_node.py +56 -0
- trilogy/core/processing/utility.py +823 -0
- trilogy/core/query_processor.py +604 -0
- trilogy/core/statements/README.md +35 -0
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +536 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +20 -0
- trilogy/core/statements/execute.py +155 -0
- trilogy/core/table_processor.py +66 -0
- trilogy/core/utility.py +8 -0
- trilogy/core/validation/README.md +46 -0
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +161 -0
- trilogy/core/validation/concept.py +146 -0
- trilogy/core/validation/datasource.py +227 -0
- trilogy/core/validation/environment.py +73 -0
- trilogy/core/validation/fix.py +256 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1432 -0
- trilogy/dialect/bigquery.py +314 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +159 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +397 -0
- trilogy/dialect/enums.py +151 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +117 -0
- trilogy/dialect/presto.py +110 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +129 -0
- trilogy/dialect/sql_server.py +137 -0
- trilogy/engine.py +48 -0
- trilogy/execution/__init__.py +17 -0
- trilogy/execution/config.py +119 -0
- trilogy/execution/state/__init__.py +0 -0
- trilogy/execution/state/exceptions.py +26 -0
- trilogy/execution/state/file_state_store.py +0 -0
- trilogy/execution/state/sqllite_state_store.py +0 -0
- trilogy/execution/state/state_store.py +406 -0
- trilogy/executor.py +692 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +135 -0
- trilogy/hooks/query_debugger.py +166 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/README.md +21 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +1069 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +8 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +2876 -0
- trilogy/parsing/render.py +775 -0
- trilogy/parsing/trilogy.lark +546 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +45 -0
- trilogy/scripts/README.md +9 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/agent.py +41 -0
- trilogy/scripts/agent_info.py +306 -0
- trilogy/scripts/common.py +432 -0
- trilogy/scripts/dependency/Cargo.lock +617 -0
- trilogy/scripts/dependency/Cargo.toml +39 -0
- trilogy/scripts/dependency/README.md +131 -0
- trilogy/scripts/dependency/build.sh +25 -0
- trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
- trilogy/scripts/dependency/src/lib.rs +16 -0
- trilogy/scripts/dependency/src/main.rs +770 -0
- trilogy/scripts/dependency/src/parser.rs +435 -0
- trilogy/scripts/dependency/src/preql.pest +208 -0
- trilogy/scripts/dependency/src/python_bindings.rs +311 -0
- trilogy/scripts/dependency/src/resolver.rs +716 -0
- trilogy/scripts/dependency/tests/base.preql +3 -0
- trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
- trilogy/scripts/dependency/tests/customer.preql +6 -0
- trilogy/scripts/dependency/tests/main.preql +9 -0
- trilogy/scripts/dependency/tests/orders.preql +7 -0
- trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
- trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
- trilogy/scripts/dependency.py +323 -0
- trilogy/scripts/display.py +555 -0
- trilogy/scripts/environment.py +59 -0
- trilogy/scripts/fmt.py +32 -0
- trilogy/scripts/ingest.py +487 -0
- trilogy/scripts/ingest_helpers/__init__.py +1 -0
- trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
- trilogy/scripts/ingest_helpers/formatting.py +93 -0
- trilogy/scripts/ingest_helpers/typing.py +161 -0
- trilogy/scripts/init.py +105 -0
- trilogy/scripts/parallel_execution.py +762 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/refresh.py +161 -0
- trilogy/scripts/run.py +79 -0
- trilogy/scripts/serve.py +202 -0
- trilogy/scripts/serve_helpers/__init__.py +41 -0
- trilogy/scripts/serve_helpers/file_discovery.py +142 -0
- trilogy/scripts/serve_helpers/index_generation.py +206 -0
- trilogy/scripts/serve_helpers/models.py +38 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/testing.py +143 -0
- trilogy/scripts/trilogy.py +75 -0
- trilogy/std/__init__.py +0 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/date.preql +13 -0
- trilogy/std/display.preql +18 -0
- trilogy/std/geography.preql +22 -0
- trilogy/std/metric.preql +15 -0
- trilogy/std/money.preql +67 -0
- trilogy/std/net.preql +14 -0
- trilogy/std/ranking.preql +7 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/semantic.preql +6 -0
- trilogy/utility.py +34 -0
trilogy/ai/prompts.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from trilogy import Environment
|
|
2
|
+
from trilogy.ai.constants import AGGREGATE_FUNCTIONS, FUNCTIONS, RULE_PROMPT
|
|
3
|
+
from trilogy.authoring import (
|
|
4
|
+
ArrayType,
|
|
5
|
+
Concept,
|
|
6
|
+
DataType,
|
|
7
|
+
MapType,
|
|
8
|
+
NumericType,
|
|
9
|
+
StructType,
|
|
10
|
+
TraitDataType,
|
|
11
|
+
)
|
|
12
|
+
from trilogy.core.models.core import DataTyped, StructComponent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_trilogy_syntax_reference() -> str:
|
|
16
|
+
return f"""Key Trilogy Syntax Rules:
|
|
17
|
+
{RULE_PROMPT}
|
|
18
|
+
|
|
19
|
+
Aggregate Functions:
|
|
20
|
+
{AGGREGATE_FUNCTIONS}
|
|
21
|
+
|
|
22
|
+
Functions:
|
|
23
|
+
{FUNCTIONS}
|
|
24
|
+
|
|
25
|
+
Valid types:
|
|
26
|
+
{[x.value for x in DataType]}
|
|
27
|
+
|
|
28
|
+
Some types may have additional metadata, which will help you understand them. For example, 'latitude', 'longitude' and 'currency' are all of type 'float', but have additional meaning."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_trilogy_prompt(intro: str | None = None, outro: str | None = None) -> str:
|
|
32
|
+
parts = []
|
|
33
|
+
if intro:
|
|
34
|
+
parts.append(intro)
|
|
35
|
+
parts.append(get_trilogy_syntax_reference())
|
|
36
|
+
if outro:
|
|
37
|
+
parts.append(outro)
|
|
38
|
+
return "\n\n".join(parts)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
TRILOGY_LEAD_IN = get_trilogy_prompt(
|
|
42
|
+
intro="You are a world-class expert in Trilogy, a SQL inspired language with similar syntax and a built in semantic layer. Use the following syntax description to help answer whatever questions they have. Often, they will be asking you to generate a query for them.",
|
|
43
|
+
outro='For any response to the user, use this format -> put your actual response within triple double quotes with thinking and justification before it, in this format (replace placeholders with relevant content): Reasoning: {reasoning} """{response}"""',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def datatype_to_field_prompt(
|
|
48
|
+
datatype: (
|
|
49
|
+
DataType
|
|
50
|
+
| TraitDataType
|
|
51
|
+
| ArrayType
|
|
52
|
+
| StructType
|
|
53
|
+
| MapType
|
|
54
|
+
| NumericType
|
|
55
|
+
| DataTyped
|
|
56
|
+
| StructComponent
|
|
57
|
+
| int
|
|
58
|
+
| float
|
|
59
|
+
| str
|
|
60
|
+
),
|
|
61
|
+
) -> str:
|
|
62
|
+
if isinstance(datatype, TraitDataType):
|
|
63
|
+
return f"{datatype_to_field_prompt(datatype.type)}({','.join(datatype.traits)})"
|
|
64
|
+
if isinstance(datatype, ArrayType):
|
|
65
|
+
return f"ARRAY<{datatype_to_field_prompt(datatype.type)}>"
|
|
66
|
+
if isinstance(datatype, StructType):
|
|
67
|
+
instantiated = []
|
|
68
|
+
for name, field_type in datatype.field_types.items():
|
|
69
|
+
if isinstance(field_type, StructComponent):
|
|
70
|
+
instantiated.append(f"{datatype_to_field_prompt(field_type.type)}")
|
|
71
|
+
else:
|
|
72
|
+
instantiated.append(f"{name}: {datatype_to_field_prompt(field_type)}")
|
|
73
|
+
fields_str = ", ".join(instantiated)
|
|
74
|
+
return f"STRUCT<{fields_str}>"
|
|
75
|
+
if isinstance(datatype, MapType):
|
|
76
|
+
return f"MAP<{datatype_to_field_prompt(datatype.key_type)}, {datatype_to_field_prompt(datatype.value_type)}>"
|
|
77
|
+
if isinstance(datatype, NumericType):
|
|
78
|
+
return f"NUMERIC({datatype.precision}, {datatype.scale})>"
|
|
79
|
+
if isinstance(datatype, DataTyped):
|
|
80
|
+
return datatype_to_field_prompt(datatype.output_datatype)
|
|
81
|
+
if isinstance(datatype, StructComponent):
|
|
82
|
+
return f"{datatype.name}: {datatype_to_field_prompt(datatype.type)}"
|
|
83
|
+
if isinstance(datatype, (int, float, str)):
|
|
84
|
+
return f"{datatype}"
|
|
85
|
+
return f"{datatype.value}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def concepts_to_fields_prompt(concepts: list[Concept]) -> str:
|
|
89
|
+
return ", ".join(
|
|
90
|
+
[
|
|
91
|
+
f"[name: {c.address} | type: {datatype_to_field_prompt(c.datatype)}]"
|
|
92
|
+
for c in concepts
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def create_query_prompt(query: str, environment: Environment) -> str:
|
|
98
|
+
fields = concepts_to_fields_prompt(list(environment.concepts.values()))
|
|
99
|
+
return f'''
|
|
100
|
+
Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: {fields}, create the best valid Trilogy query to answer the following user input: "{query}" Return the query within triple double quotes with your thinking and justification before it, so of this form as a jinja template: Reasoning: {{reasoning_placeholder}} """{{trilogy}}""". Example: Because the user asked for sales by year, and revenue is the best sales related field available, we can aggregate revenue by year: """SELECT order.year, sum(revenue) as year_revenue order by order.year asc;"""'''
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from trilogy.ai.enums import Provider
|
|
5
|
+
from trilogy.ai.models import LLMMessage, LLMResponse, UsageDict
|
|
6
|
+
from trilogy.constants import logger
|
|
7
|
+
|
|
8
|
+
from .base import RETRYABLE_CODES, LLMProvider, LLMRequestOptions
|
|
9
|
+
from .utils import RetryOptions, fetch_with_retry
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAX_TOKENS = 10000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AnthropicProvider(LLMProvider):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
model: str,
|
|
19
|
+
api_key: str | None = None,
|
|
20
|
+
retry_options: Optional[RetryOptions] = None,
|
|
21
|
+
):
|
|
22
|
+
api_key = api_key or environ.get("ANTHROPIC_API_KEY")
|
|
23
|
+
if not api_key:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
"API key argument or environment variable ANTHROPIC_API_KEY is required"
|
|
26
|
+
)
|
|
27
|
+
super().__init__(name, api_key, model, Provider.ANTHROPIC)
|
|
28
|
+
self.base_completion_url = "https://api.anthropic.com/v1/messages"
|
|
29
|
+
self.base_model_url = "https://api.anthropic.com/v1/models"
|
|
30
|
+
self.models: List[str] = []
|
|
31
|
+
self.type = Provider.ANTHROPIC
|
|
32
|
+
self.retry_options = retry_options or RetryOptions(
|
|
33
|
+
max_retries=5,
|
|
34
|
+
initial_delay_ms=5000,
|
|
35
|
+
retry_status_codes=RETRYABLE_CODES,
|
|
36
|
+
on_retry=lambda attempt, delay_ms, error: logger.info(
|
|
37
|
+
f"Anthropic API retry attempt {attempt} after {delay_ms}ms delay due to error: {str(error)}"
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def generate_completion(
|
|
42
|
+
self, options: LLMRequestOptions, history: List[LLMMessage]
|
|
43
|
+
) -> LLMResponse:
|
|
44
|
+
try:
|
|
45
|
+
import httpx
|
|
46
|
+
except ImportError:
|
|
47
|
+
raise ImportError(
|
|
48
|
+
"Missing httpx. Install pytrilogy[ai] to use AnthropicProvider."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Separate system messages from user/assistant messages
|
|
52
|
+
system_messages = [msg.content for msg in history if msg.role == "system"]
|
|
53
|
+
conversation_messages = [
|
|
54
|
+
{"role": msg.role, "content": msg.content}
|
|
55
|
+
for msg in history
|
|
56
|
+
if msg.role != "system"
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
|
|
61
|
+
def make_request():
|
|
62
|
+
with httpx.Client(timeout=60) as client:
|
|
63
|
+
payload = {
|
|
64
|
+
"model": self.model,
|
|
65
|
+
"messages": conversation_messages,
|
|
66
|
+
"max_tokens": options.max_tokens or DEFAULT_MAX_TOKENS,
|
|
67
|
+
# "temperature": options.temperature or 0.7,
|
|
68
|
+
# "top_p": options.top_p if hasattr(options, "top_p") else 1.0,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Add system parameter if there are system messages
|
|
72
|
+
if system_messages:
|
|
73
|
+
# Combine multiple system messages with newlines
|
|
74
|
+
payload["system"] = "\n\n".join(system_messages)
|
|
75
|
+
|
|
76
|
+
response = client.post(
|
|
77
|
+
url=self.base_completion_url,
|
|
78
|
+
headers={
|
|
79
|
+
"Content-Type": "application/json",
|
|
80
|
+
"x-api-key": self.api_key,
|
|
81
|
+
"anthropic-version": "2023-06-01",
|
|
82
|
+
},
|
|
83
|
+
json=payload,
|
|
84
|
+
)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
return response.json()
|
|
87
|
+
|
|
88
|
+
data = fetch_with_retry(make_request, self.retry_options)
|
|
89
|
+
|
|
90
|
+
return LLMResponse(
|
|
91
|
+
text=data["content"][0]["text"],
|
|
92
|
+
usage=UsageDict(
|
|
93
|
+
prompt_tokens=data["usage"]["input_tokens"],
|
|
94
|
+
completion_tokens=data["usage"]["output_tokens"],
|
|
95
|
+
total_tokens=data["usage"]["input_tokens"]
|
|
96
|
+
+ data["usage"]["output_tokens"],
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
except httpx.HTTPStatusError as error:
|
|
101
|
+
error_detail = error.response.text
|
|
102
|
+
raise Exception(
|
|
103
|
+
f"Anthropic API error ({error.response.status_code}): {error_detail}"
|
|
104
|
+
)
|
|
105
|
+
except Exception as error:
|
|
106
|
+
raise Exception(f"Anthropic API error: {str(error)}")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from trilogy.ai.enums import Provider
|
|
5
|
+
from trilogy.ai.models import LLMMessage, LLMRequestOptions, LLMResponse
|
|
6
|
+
|
|
7
|
+
RETRYABLE_CODES = [429, 500, 502, 503, 504]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LLMProvider(ABC):
|
|
11
|
+
def __init__(self, name: str, api_key: str, model: str, provider: Provider):
|
|
12
|
+
self.api_key = api_key
|
|
13
|
+
self.models: List[str] = []
|
|
14
|
+
self.name = name
|
|
15
|
+
self.model = model
|
|
16
|
+
self.type = provider
|
|
17
|
+
self.error: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
# Abstract method to be implemented by specific providers
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def generate_completion(
|
|
22
|
+
self, options: LLMRequestOptions, history: List[LLMMessage]
|
|
23
|
+
) -> LLMResponse:
|
|
24
|
+
pass
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from trilogy.ai.enums import Provider
|
|
5
|
+
from trilogy.ai.models import LLMMessage, LLMResponse, UsageDict
|
|
6
|
+
from trilogy.constants import logger
|
|
7
|
+
|
|
8
|
+
from .base import RETRYABLE_CODES, LLMProvider, LLMRequestOptions
|
|
9
|
+
from .utils import RetryOptions, fetch_with_retry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GoogleProvider(LLMProvider):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
name: str,
|
|
16
|
+
model: str,
|
|
17
|
+
api_key: str | None = None,
|
|
18
|
+
retry_options: Optional[RetryOptions] = None,
|
|
19
|
+
):
|
|
20
|
+
api_key = api_key or environ.get("GOOGLE_API_KEY")
|
|
21
|
+
if not api_key:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"API key argument or environment variable GOOGLE_API_KEY is required"
|
|
24
|
+
)
|
|
25
|
+
super().__init__(name, api_key, model, Provider.GOOGLE)
|
|
26
|
+
self.base_model_url = "https://generativelanguage.googleapis.com/v1/models"
|
|
27
|
+
self.base_completion_url = "https://generativelanguage.googleapis.com/v1beta"
|
|
28
|
+
self.models: List[str] = []
|
|
29
|
+
self.type = Provider.GOOGLE
|
|
30
|
+
self.retry_options = retry_options or RetryOptions(
|
|
31
|
+
max_retries=3,
|
|
32
|
+
initial_delay_ms=30000,
|
|
33
|
+
retry_status_codes=RETRYABLE_CODES,
|
|
34
|
+
on_retry=lambda attempt, delay_ms, error: logger.info(
|
|
35
|
+
f"Google API retry attempt {attempt} after {delay_ms}ms delay due to error: {str(error)}"
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def _convert_to_gemini_history(
|
|
40
|
+
self, messages: List[LLMMessage]
|
|
41
|
+
) -> List[Dict[str, Any]]:
|
|
42
|
+
"""Convert standard message format to Gemini format."""
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
"role": "model" if msg.role == "assistant" else "user",
|
|
46
|
+
"parts": [{"text": msg.content}],
|
|
47
|
+
}
|
|
48
|
+
for msg in messages
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def generate_completion(
|
|
52
|
+
self, options: LLMRequestOptions, history: List[LLMMessage]
|
|
53
|
+
) -> LLMResponse:
|
|
54
|
+
try:
|
|
55
|
+
import httpx
|
|
56
|
+
except ImportError:
|
|
57
|
+
raise ImportError(
|
|
58
|
+
"Missing httpx. Install pytrilogy[ai] to use GoogleProvider."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Convert messages to Gemini format
|
|
62
|
+
gemini_history = self._convert_to_gemini_history(history)
|
|
63
|
+
|
|
64
|
+
# Separate system message if present
|
|
65
|
+
system_instruction = None
|
|
66
|
+
contents = gemini_history
|
|
67
|
+
|
|
68
|
+
# Check if first message is a system message
|
|
69
|
+
if history and history[0].role == "system":
|
|
70
|
+
system_instruction = {"parts": [{"text": history[0].content}]}
|
|
71
|
+
contents = gemini_history[1:] # Remove system message from history
|
|
72
|
+
|
|
73
|
+
# Build the request URL
|
|
74
|
+
url = f"{self.base_completion_url}/models/{self.model}:generateContent"
|
|
75
|
+
|
|
76
|
+
# Build request body
|
|
77
|
+
request_body: Dict[str, Any] = {"contents": contents, "generationConfig": {}}
|
|
78
|
+
|
|
79
|
+
# Add system instruction if present
|
|
80
|
+
if system_instruction:
|
|
81
|
+
request_body["systemInstruction"] = system_instruction
|
|
82
|
+
|
|
83
|
+
# Add generation config options
|
|
84
|
+
if options.temperature is not None:
|
|
85
|
+
request_body["generationConfig"]["temperature"] = options.temperature
|
|
86
|
+
|
|
87
|
+
if options.max_tokens is not None:
|
|
88
|
+
request_body["generationConfig"]["maxOutputTokens"] = options.max_tokens
|
|
89
|
+
|
|
90
|
+
if options.top_p is not None:
|
|
91
|
+
request_body["generationConfig"]["topP"] = options.top_p
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Make the API request with retry logic using a lambda
|
|
95
|
+
|
|
96
|
+
def fetch_function() -> Dict[str, Any]:
|
|
97
|
+
with httpx.Client(timeout=60) as client:
|
|
98
|
+
resp = client.post(
|
|
99
|
+
url,
|
|
100
|
+
headers={
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"x-goog-api-key": self.api_key,
|
|
103
|
+
},
|
|
104
|
+
json=request_body,
|
|
105
|
+
)
|
|
106
|
+
resp.raise_for_status()
|
|
107
|
+
return resp.json()
|
|
108
|
+
|
|
109
|
+
data = fetch_with_retry(
|
|
110
|
+
fetch_fn=fetch_function,
|
|
111
|
+
options=self.retry_options,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Extract text from response
|
|
115
|
+
candidates = data.get("candidates", [])
|
|
116
|
+
if not candidates:
|
|
117
|
+
raise Exception("No candidates returned from Google API")
|
|
118
|
+
|
|
119
|
+
content = candidates[0].get("content", {})
|
|
120
|
+
parts = content.get("parts", [])
|
|
121
|
+
|
|
122
|
+
if not parts:
|
|
123
|
+
raise Exception("No parts in response content")
|
|
124
|
+
|
|
125
|
+
text = parts[0].get("text", "")
|
|
126
|
+
|
|
127
|
+
# Extract usage metadata
|
|
128
|
+
usage_metadata = data.get("usageMetadata", {})
|
|
129
|
+
prompt_tokens = usage_metadata.get("promptTokenCount", 0)
|
|
130
|
+
completion_tokens = usage_metadata.get("candidatesTokenCount", 0)
|
|
131
|
+
|
|
132
|
+
return LLMResponse(
|
|
133
|
+
text=text,
|
|
134
|
+
usage=UsageDict(
|
|
135
|
+
prompt_tokens=prompt_tokens,
|
|
136
|
+
completion_tokens=completion_tokens,
|
|
137
|
+
total_tokens=prompt_tokens + completion_tokens,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
except httpx.HTTPStatusError as error:
|
|
141
|
+
error_detail = error.response.text
|
|
142
|
+
raise Exception(
|
|
143
|
+
f"Google API error ({error.response.status_code}): {error_detail}"
|
|
144
|
+
)
|
|
145
|
+
except Exception as error:
|
|
146
|
+
raise Exception(f"Google API error: {str(error)}")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from os import environ
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from trilogy.ai.enums import Provider
|
|
5
|
+
from trilogy.ai.models import LLMMessage, LLMResponse, UsageDict
|
|
6
|
+
from trilogy.constants import logger
|
|
7
|
+
|
|
8
|
+
from .base import RETRYABLE_CODES, LLMProvider, LLMRequestOptions
|
|
9
|
+
from .utils import RetryOptions, fetch_with_retry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenAIProvider(LLMProvider):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
name: str,
|
|
16
|
+
model: str,
|
|
17
|
+
api_key: str | None = None,
|
|
18
|
+
retry_options: Optional[RetryOptions] = None,
|
|
19
|
+
):
|
|
20
|
+
api_key = api_key or environ.get("OPENAI_API_KEY")
|
|
21
|
+
if not api_key:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"API key argument or environment variable OPENAI_API_KEY is required"
|
|
24
|
+
)
|
|
25
|
+
super().__init__(name, api_key, model, Provider.OPENAI)
|
|
26
|
+
self.base_completion_url = "https://api.openai.com/v1/chat/completions"
|
|
27
|
+
self.base_model_url = "https://api.openai.com/v1/models"
|
|
28
|
+
self.models: List[str] = []
|
|
29
|
+
self.type = Provider.OPENAI
|
|
30
|
+
|
|
31
|
+
self.retry_options = retry_options or RetryOptions(
|
|
32
|
+
max_retries=3,
|
|
33
|
+
initial_delay_ms=1000,
|
|
34
|
+
retry_status_codes=RETRYABLE_CODES,
|
|
35
|
+
on_retry=lambda attempt, delay_ms, error: logger.info(
|
|
36
|
+
f"Retry attempt {attempt} after {delay_ms}ms delay due to error: {str(error)}"
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def generate_completion(
|
|
41
|
+
self, options: LLMRequestOptions, history: List[LLMMessage]
|
|
42
|
+
) -> LLMResponse:
|
|
43
|
+
try:
|
|
44
|
+
import httpx
|
|
45
|
+
except ImportError:
|
|
46
|
+
raise ImportError(
|
|
47
|
+
"Missing httpx. Install pytrilogy[ai] to use OpenAIProvider."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
messages: List[dict] = []
|
|
51
|
+
messages = [{"role": msg.role, "content": msg.content} for msg in history]
|
|
52
|
+
try:
|
|
53
|
+
|
|
54
|
+
def make_request():
|
|
55
|
+
with httpx.Client(timeout=30) as client:
|
|
56
|
+
payload = {
|
|
57
|
+
"model": self.model,
|
|
58
|
+
"messages": messages,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
response = client.post(
|
|
62
|
+
url=self.base_completion_url,
|
|
63
|
+
headers={
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
66
|
+
},
|
|
67
|
+
json=payload,
|
|
68
|
+
)
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
return response.json()
|
|
71
|
+
|
|
72
|
+
data = fetch_with_retry(make_request, self.retry_options)
|
|
73
|
+
return LLMResponse(
|
|
74
|
+
text=data["choices"][0]["message"]["content"],
|
|
75
|
+
usage=UsageDict(
|
|
76
|
+
prompt_tokens=data["usage"]["prompt_tokens"],
|
|
77
|
+
completion_tokens=data["usage"]["completion_tokens"],
|
|
78
|
+
total_tokens=data["usage"]["total_tokens"],
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
except httpx.HTTPStatusError as error:
|
|
82
|
+
# Capture the response body text
|
|
83
|
+
error_detail = error.response.text
|
|
84
|
+
raise Exception(
|
|
85
|
+
f"OpenAI API error ({error.response.status_code}): {error_detail}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
except Exception as error:
|
|
89
|
+
raise Exception(f"OpenAI API error: {str(error)}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Callable, List, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class RetryOptions:
|
|
10
|
+
max_retries: int = 3
|
|
11
|
+
initial_delay_ms: int = 1000
|
|
12
|
+
retry_status_codes: List[int] = field(
|
|
13
|
+
default_factory=lambda: [429, 500, 502, 503, 504, 525]
|
|
14
|
+
)
|
|
15
|
+
on_retry: Callable[[int, int, Exception], None] | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fetch_with_retry(fetch_fn: Callable[[], T], options: RetryOptions) -> T:
|
|
19
|
+
from httpx import HTTPError
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
Retry a fetch operation with exponential backoff.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
fetch_fn: Function that performs the fetch operation
|
|
26
|
+
options: Retry configuration options
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The result from the successful fetch operation
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
The last exception encountered if all retries fail
|
|
33
|
+
"""
|
|
34
|
+
from httpx import HTTPStatusError, TimeoutException
|
|
35
|
+
|
|
36
|
+
last_error = None
|
|
37
|
+
delay_ms = options.initial_delay_ms
|
|
38
|
+
|
|
39
|
+
for attempt in range(options.max_retries + 1):
|
|
40
|
+
try:
|
|
41
|
+
return fetch_fn()
|
|
42
|
+
except (HTTPError, TimeoutException, Exception) as error:
|
|
43
|
+
last_error = error
|
|
44
|
+
should_retry = False
|
|
45
|
+
|
|
46
|
+
if isinstance(error, HTTPStatusError):
|
|
47
|
+
if (
|
|
48
|
+
options.retry_status_codes
|
|
49
|
+
and error.response.status_code in options.retry_status_codes
|
|
50
|
+
):
|
|
51
|
+
should_retry = True
|
|
52
|
+
elif isinstance(error, TimeoutException):
|
|
53
|
+
should_retry = True
|
|
54
|
+
if not should_retry or attempt >= options.max_retries:
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
# Call the retry callback if provided
|
|
58
|
+
if options.on_retry:
|
|
59
|
+
options.on_retry(attempt + 1, delay_ms, error)
|
|
60
|
+
|
|
61
|
+
# Wait before retrying with exponential backoff
|
|
62
|
+
time.sleep(delay_ms / 1000.0)
|
|
63
|
+
delay_ms *= 2 # backoff
|
|
64
|
+
|
|
65
|
+
# This should never be reached, but just in case
|
|
66
|
+
if last_error:
|
|
67
|
+
raise last_error
|
|
68
|
+
raise Exception("Retry logic failed unexpectedly")
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from trilogy.constants import DEFAULT_NAMESPACE
|
|
2
|
+
from trilogy.core.enums import (
|
|
3
|
+
BooleanOperator,
|
|
4
|
+
ComparisonOperator,
|
|
5
|
+
FunctionClass,
|
|
6
|
+
FunctionType,
|
|
7
|
+
InfiniteFunctionArgs,
|
|
8
|
+
Ordering,
|
|
9
|
+
Purpose,
|
|
10
|
+
)
|
|
11
|
+
from trilogy.core.functions import FunctionFactory
|
|
12
|
+
from trilogy.core.models.author import (
|
|
13
|
+
AggregateWrapper,
|
|
14
|
+
CaseElse,
|
|
15
|
+
CaseWhen,
|
|
16
|
+
Comment,
|
|
17
|
+
Comparison,
|
|
18
|
+
Concept,
|
|
19
|
+
ConceptRef,
|
|
20
|
+
Conditional,
|
|
21
|
+
FilterItem,
|
|
22
|
+
Function,
|
|
23
|
+
FunctionCallWrapper,
|
|
24
|
+
HavingClause,
|
|
25
|
+
MagicConstants,
|
|
26
|
+
Metadata,
|
|
27
|
+
MultiSelectLineage,
|
|
28
|
+
OrderBy,
|
|
29
|
+
OrderItem,
|
|
30
|
+
Parenthetical,
|
|
31
|
+
RowsetItem,
|
|
32
|
+
SubselectComparison,
|
|
33
|
+
WhereClause,
|
|
34
|
+
WindowItem,
|
|
35
|
+
WindowItemOrder,
|
|
36
|
+
WindowItemOver,
|
|
37
|
+
WindowOrder,
|
|
38
|
+
WindowType,
|
|
39
|
+
)
|
|
40
|
+
from trilogy.core.models.core import (
|
|
41
|
+
ArrayType,
|
|
42
|
+
DataType,
|
|
43
|
+
ListWrapper,
|
|
44
|
+
MapType,
|
|
45
|
+
NumericType,
|
|
46
|
+
StructType,
|
|
47
|
+
TraitDataType,
|
|
48
|
+
)
|
|
49
|
+
from trilogy.core.models.datasource import Address, Datasource, DatasourceMetadata
|
|
50
|
+
from trilogy.core.models.environment import DictImportResolver, Environment
|
|
51
|
+
from trilogy.core.statements.author import (
|
|
52
|
+
STATEMENT_TYPES,
|
|
53
|
+
ConceptDeclarationStatement,
|
|
54
|
+
ConceptTransform,
|
|
55
|
+
CopyStatement,
|
|
56
|
+
Grain,
|
|
57
|
+
HasUUID,
|
|
58
|
+
ImportStatement,
|
|
59
|
+
MultiSelectStatement,
|
|
60
|
+
PersistStatement,
|
|
61
|
+
RawSQLStatement,
|
|
62
|
+
RowsetDerivationStatement,
|
|
63
|
+
SelectItem,
|
|
64
|
+
SelectStatement,
|
|
65
|
+
ShowCategory,
|
|
66
|
+
ShowStatement,
|
|
67
|
+
ValidateStatement,
|
|
68
|
+
)
|
|
69
|
+
from trilogy.parsing.common import arbitrary_to_concept, arg_to_datatype
|
|
70
|
+
from trilogy.parsing.render import Renderer
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
# trilogy.constants
|
|
74
|
+
"DEFAULT_NAMESPACE",
|
|
75
|
+
# trilogy.core.enums
|
|
76
|
+
"BooleanOperator",
|
|
77
|
+
"ComparisonOperator",
|
|
78
|
+
"Comment",
|
|
79
|
+
"FunctionClass",
|
|
80
|
+
"FunctionType",
|
|
81
|
+
"InfiniteFunctionArgs",
|
|
82
|
+
"Ordering",
|
|
83
|
+
"Purpose",
|
|
84
|
+
# trilogy.core.functions
|
|
85
|
+
"FunctionFactory",
|
|
86
|
+
# trilogy.core.models.author
|
|
87
|
+
"AggregateWrapper",
|
|
88
|
+
"CaseElse",
|
|
89
|
+
"CaseWhen",
|
|
90
|
+
"Comparison",
|
|
91
|
+
"Concept",
|
|
92
|
+
"ConceptRef",
|
|
93
|
+
"Conditional",
|
|
94
|
+
"FilterItem",
|
|
95
|
+
"Function",
|
|
96
|
+
"FunctionCallWrapper",
|
|
97
|
+
"HavingClause",
|
|
98
|
+
"MagicConstants",
|
|
99
|
+
"Metadata",
|
|
100
|
+
"MultiSelectLineage",
|
|
101
|
+
"OrderBy",
|
|
102
|
+
"OrderItem",
|
|
103
|
+
"Parenthetical",
|
|
104
|
+
"RowsetItem",
|
|
105
|
+
"SubselectComparison",
|
|
106
|
+
"WhereClause",
|
|
107
|
+
"WindowItem",
|
|
108
|
+
"WindowItemOrder",
|
|
109
|
+
"WindowItemOver",
|
|
110
|
+
"WindowOrder",
|
|
111
|
+
"WindowType",
|
|
112
|
+
# trilogy.core.models.core
|
|
113
|
+
"ArrayType",
|
|
114
|
+
"DataType",
|
|
115
|
+
"ListWrapper",
|
|
116
|
+
"MapType",
|
|
117
|
+
"NumericType",
|
|
118
|
+
"StructType",
|
|
119
|
+
"TraitDataType",
|
|
120
|
+
# trilogy.core.models.datasource
|
|
121
|
+
"Address",
|
|
122
|
+
"Datasource",
|
|
123
|
+
"DatasourceMetadata",
|
|
124
|
+
# trilogy.core.models.environment
|
|
125
|
+
"Environment",
|
|
126
|
+
# trilogy.core.statements.author
|
|
127
|
+
"ConceptDeclarationStatement",
|
|
128
|
+
"ConceptTransform",
|
|
129
|
+
"CopyStatement",
|
|
130
|
+
"Grain",
|
|
131
|
+
"HasUUID",
|
|
132
|
+
"ImportStatement",
|
|
133
|
+
"MultiSelectStatement",
|
|
134
|
+
"PersistStatement",
|
|
135
|
+
"RawSQLStatement",
|
|
136
|
+
"RowsetDerivationStatement",
|
|
137
|
+
"SelectItem",
|
|
138
|
+
"SelectStatement",
|
|
139
|
+
"ShowCategory",
|
|
140
|
+
"ShowStatement",
|
|
141
|
+
"ValidateStatement",
|
|
142
|
+
# trilogy.parsing.common
|
|
143
|
+
"arbitrary_to_concept",
|
|
144
|
+
"arg_to_datatype",
|
|
145
|
+
"STATEMENT_TYPES",
|
|
146
|
+
"DictImportResolver",
|
|
147
|
+
"Renderer",
|
|
148
|
+
]
|