pytrilogy 0.0.3.107__tar.gz → 0.0.3.109__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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (174) hide show
  1. {pytrilogy-0.0.3.107/pytrilogy.egg-info → pytrilogy-0.0.3.109}/PKG-INFO +69 -1
  2. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/README.md +66 -0
  3. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109/pytrilogy.egg-info}/PKG-INFO +69 -1
  4. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/pytrilogy.egg-info/SOURCES.txt +13 -0
  5. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/pytrilogy.egg-info/requires.txt +3 -0
  6. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/setup.py +1 -0
  7. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/__init__.py +1 -1
  8. pytrilogy-0.0.3.109/trilogy/ai/__init__.py +19 -0
  9. pytrilogy-0.0.3.109/trilogy/ai/constants.py +92 -0
  10. pytrilogy-0.0.3.109/trilogy/ai/conversation.py +99 -0
  11. pytrilogy-0.0.3.109/trilogy/ai/enums.py +7 -0
  12. pytrilogy-0.0.3.109/trilogy/ai/execute.py +50 -0
  13. pytrilogy-0.0.3.109/trilogy/ai/models.py +34 -0
  14. pytrilogy-0.0.3.109/trilogy/ai/prompts.py +30 -0
  15. pytrilogy-0.0.3.109/trilogy/ai/providers/anthropic.py +105 -0
  16. pytrilogy-0.0.3.109/trilogy/ai/providers/base.py +22 -0
  17. pytrilogy-0.0.3.109/trilogy/ai/providers/google.py +142 -0
  18. pytrilogy-0.0.3.109/trilogy/ai/providers/openai.py +88 -0
  19. pytrilogy-0.0.3.109/trilogy/ai/providers/utils.py +68 -0
  20. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/select_merge_node.py +9 -4
  21. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/executor.py +35 -7
  22. pytrilogy-0.0.3.109/trilogy/std/__init__.py +0 -0
  23. pytrilogy-0.0.3.109/trilogy/std/display.preql +18 -0
  24. pytrilogy-0.0.3.107/trilogy/std/display.preql +0 -9
  25. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/LICENSE.md +0 -0
  26. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/pytrilogy.egg-info/dependency_links.txt +0 -0
  28. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/pytrilogy.egg-info/entry_points.txt +0 -0
  29. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/pytrilogy.egg-info/top_level.txt +0 -0
  30. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/setup.cfg +0 -0
  31. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_datatypes.py +0 -0
  32. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_declarations.py +0 -0
  33. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_derived_concepts.py +0 -0
  34. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_discovery_nodes.py +0 -0
  35. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_enums.py +0 -0
  36. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_environment.py +0 -0
  37. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_execute_models.py +0 -0
  38. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_executor.py +0 -0
  39. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_failure.py +0 -0
  40. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_functions.py +0 -0
  41. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_imports.py +0 -0
  42. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_metadata.py +0 -0
  43. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_models.py +0 -0
  44. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_multi_join_assignments.py +0 -0
  45. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_parse_engine.py +0 -0
  46. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_parsing.py +0 -0
  47. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_parsing_failures.py +0 -0
  48. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_partial_handling.py +0 -0
  49. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_query_processing.py +0 -0
  50. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_query_render.py +0 -0
  51. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_select.py +0 -0
  52. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_show.py +0 -0
  53. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_statements.py +0 -0
  54. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_typing.py +0 -0
  55. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_undefined_concept.py +0 -0
  56. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_user_functions.py +0 -0
  57. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_validators.py +0 -0
  58. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/tests/test_where_clause.py +0 -0
  59. {pytrilogy-0.0.3.107/trilogy/core → pytrilogy-0.0.3.109/trilogy/ai/providers}/__init__.py +0 -0
  60. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/authoring/__init__.py +0 -0
  61. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/constants.py +0 -0
  62. {pytrilogy-0.0.3.107/trilogy/core/models → pytrilogy-0.0.3.109/trilogy/core}/__init__.py +0 -0
  63. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/constants.py +0 -0
  64. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/enums.py +0 -0
  65. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/env_processor.py +0 -0
  66. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/environment_helpers.py +0 -0
  67. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/ergonomics.py +0 -0
  68. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/exceptions.py +0 -0
  69. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/functions.py +0 -0
  70. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/graph_models.py +0 -0
  71. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/internal.py +0 -0
  72. {pytrilogy-0.0.3.107/trilogy/core/processing → pytrilogy-0.0.3.109/trilogy/core/models}/__init__.py +0 -0
  73. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/author.py +0 -0
  74. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/build.py +0 -0
  75. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/build_environment.py +0 -0
  76. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/core.py +0 -0
  77. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/datasource.py +0 -0
  78. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/environment.py +0 -0
  79. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/models/execute.py +0 -0
  80. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/optimization.py +0 -0
  81. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/optimizations/__init__.py +0 -0
  82. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/optimizations/base_optimization.py +0 -0
  83. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/optimizations/hide_unused_concept.py +0 -0
  84. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/optimizations/inline_datasource.py +0 -0
  85. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  86. {pytrilogy-0.0.3.107/trilogy/core/processing/node_generators/select_helpers → pytrilogy-0.0.3.109/trilogy/core/processing}/__init__.py +0 -0
  87. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/concept_strategies_v3.py +0 -0
  88. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/discovery_node_factory.py +0 -0
  89. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/discovery_utility.py +0 -0
  90. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/discovery_validation.py +0 -0
  91. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/graph_utils.py +0 -0
  92. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/__init__.py +0 -0
  93. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  94. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/common.py +0 -0
  95. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/constant_node.py +0 -0
  96. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/filter_node.py +0 -0
  97. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/group_node.py +0 -0
  98. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  99. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  100. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  101. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  102. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  103. {pytrilogy-0.0.3.107/trilogy/core/statements → pytrilogy-0.0.3.109/trilogy/core/processing/node_generators/select_helpers}/__init__.py +0 -0
  104. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  105. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/select_node.py +0 -0
  106. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  107. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/union_node.py +0 -0
  108. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  109. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/node_generators/window_node.py +0 -0
  110. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/__init__.py +0 -0
  111. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/base_node.py +0 -0
  112. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/filter_node.py +0 -0
  113. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/group_node.py +0 -0
  114. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/merge_node.py +0 -0
  115. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  116. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  117. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/union_node.py +0 -0
  118. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  119. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/nodes/window_node.py +0 -0
  120. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/processing/utility.py +0 -0
  121. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/query_processor.py +0 -0
  122. {pytrilogy-0.0.3.107/trilogy/core/validation → pytrilogy-0.0.3.109/trilogy/core/statements}/__init__.py +0 -0
  123. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/statements/author.py +0 -0
  124. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/statements/build.py +0 -0
  125. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/statements/common.py +0 -0
  126. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/statements/execute.py +0 -0
  127. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/utility.py +0 -0
  128. {pytrilogy-0.0.3.107/trilogy/dialect → pytrilogy-0.0.3.109/trilogy/core/validation}/__init__.py +0 -0
  129. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/validation/common.py +0 -0
  130. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/validation/concept.py +0 -0
  131. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/validation/datasource.py +0 -0
  132. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/validation/environment.py +0 -0
  133. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/core/validation/fix.py +0 -0
  134. {pytrilogy-0.0.3.107/trilogy/metadata → pytrilogy-0.0.3.109/trilogy/dialect}/__init__.py +0 -0
  135. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/base.py +0 -0
  136. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/bigquery.py +0 -0
  137. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/common.py +0 -0
  138. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/config.py +0 -0
  139. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/dataframe.py +0 -0
  140. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/duckdb.py +0 -0
  141. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/enums.py +0 -0
  142. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/metadata.py +0 -0
  143. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/postgres.py +0 -0
  144. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/presto.py +0 -0
  145. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/snowflake.py +0 -0
  146. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/dialect/sql_server.py +0 -0
  147. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/engine.py +0 -0
  148. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/hooks/__init__.py +0 -0
  149. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/hooks/base_hook.py +0 -0
  150. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/hooks/graph_hook.py +0 -0
  151. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/hooks/query_debugger.py +0 -0
  152. {pytrilogy-0.0.3.107/trilogy/parsing → pytrilogy-0.0.3.109/trilogy/metadata}/__init__.py +0 -0
  153. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parser.py +0 -0
  154. {pytrilogy-0.0.3.107/trilogy/scripts → pytrilogy-0.0.3.109/trilogy/parsing}/__init__.py +0 -0
  155. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/common.py +0 -0
  156. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/config.py +0 -0
  157. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/exceptions.py +0 -0
  158. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/helpers.py +0 -0
  159. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/parse_engine.py +0 -0
  160. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/render.py +0 -0
  161. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/parsing/trilogy.lark +0 -0
  162. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/py.typed +0 -0
  163. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/render.py +0 -0
  164. {pytrilogy-0.0.3.107/trilogy/std → pytrilogy-0.0.3.109/trilogy/scripts}/__init__.py +0 -0
  165. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/scripts/trilogy.py +0 -0
  166. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/color.preql +0 -0
  167. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/date.preql +0 -0
  168. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/geography.preql +0 -0
  169. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/metric.preql +0 -0
  170. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/money.preql +0 -0
  171. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/net.preql +0 -0
  172. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/ranking.preql +0 -0
  173. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/std/report.preql +0 -0
  174. {pytrilogy-0.0.3.107 → pytrilogy-0.0.3.109}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.107
3
+ Version: 0.0.3.109
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -28,6 +28,8 @@ Provides-Extra: bigquery
28
28
  Requires-Dist: sqlalchemy-bigquery; extra == "bigquery"
29
29
  Provides-Extra: snowflake
30
30
  Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
31
+ Provides-Extra: ai
32
+ Requires-Dist: httpx; extra == "ai"
31
33
  Dynamic: author-email
32
34
  Dynamic: classifier
33
35
  Dynamic: description
@@ -113,6 +115,31 @@ ORDER BY
113
115
  LIMIT 10;
114
116
  ```
115
117
 
118
+ ## Trilogy is Easy to Write
119
+ For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
120
+
121
+ (full code in the python API section.)
122
+
123
+ ```python
124
+ query = text_to_query(
125
+ executor.environment,
126
+ "number of flights by month in 2005",
127
+ Provider.OPENAI,
128
+ "gpt-5-chat-latest",
129
+ api_key,
130
+ )
131
+
132
+ # get a ready to run query
133
+ print(query)
134
+ # typical output
135
+ '''where local.dep_time.year = 2020
136
+ select
137
+ local.dep_time.month,
138
+ count(local.id2) as number_of_flights
139
+ order by
140
+ local.dep_time.month asc;'''
141
+ ```
142
+
116
143
  ## Goals
117
144
 
118
145
  Versus SQL, Trilogy aims to:
@@ -264,6 +291,47 @@ for row in results:
264
291
  print(x)
265
292
  ```
266
293
 
294
+ ### LLM Usage
295
+
296
+ Connect to your favorite provider and generate queries with confidence and high accuracy.
297
+
298
+ ```python
299
+ from trilogy import Environment, Dialects
300
+ from trilogy.ai import Provider, text_to_query
301
+ import os
302
+
303
+ executor = Dialects.DUCK_DB.default_executor(
304
+ environment=Environment(working_path=Path(__file__).parent)
305
+ )
306
+
307
+ api_key = os.environ.get(OPENAI_API_KEY)
308
+ if not api_key:
309
+ raise ValueError("OPENAI_API_KEY required for gpt generation")
310
+ # load a model
311
+ executor.parse_file("flight.preql")
312
+ # create tables in the DB if needed
313
+ executor.execute_file("setup.sql")
314
+ # generate a query
315
+ query = text_to_query(
316
+ executor.environment,
317
+ "number of flights by month in 2005",
318
+ Provider.OPENAI,
319
+ "gpt-5-chat-latest",
320
+ api_key,
321
+ )
322
+
323
+ # print the generated trilogy query
324
+ print(query)
325
+ # run it
326
+ results = executor.execute_text(query)[-1].fetchall()
327
+ assert len(results) == 12
328
+
329
+ for row in results:
330
+ # all monthly flights are between 5000 and 7000
331
+ assert row[1] > 5000 and row[1] < 7000, row
332
+
333
+ ```
334
+
267
335
  ### CLI Usage
268
336
 
269
337
  Trilogy can be run through a CLI tool, also named 'trilogy'.
@@ -74,6 +74,31 @@ ORDER BY
74
74
  LIMIT 10;
75
75
  ```
76
76
 
77
+ ## Trilogy is Easy to Write
78
+ For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
79
+
80
+ (full code in the python API section.)
81
+
82
+ ```python
83
+ query = text_to_query(
84
+ executor.environment,
85
+ "number of flights by month in 2005",
86
+ Provider.OPENAI,
87
+ "gpt-5-chat-latest",
88
+ api_key,
89
+ )
90
+
91
+ # get a ready to run query
92
+ print(query)
93
+ # typical output
94
+ '''where local.dep_time.year = 2020
95
+ select
96
+ local.dep_time.month,
97
+ count(local.id2) as number_of_flights
98
+ order by
99
+ local.dep_time.month asc;'''
100
+ ```
101
+
77
102
  ## Goals
78
103
 
79
104
  Versus SQL, Trilogy aims to:
@@ -225,6 +250,47 @@ for row in results:
225
250
  print(x)
226
251
  ```
227
252
 
253
+ ### LLM Usage
254
+
255
+ Connect to your favorite provider and generate queries with confidence and high accuracy.
256
+
257
+ ```python
258
+ from trilogy import Environment, Dialects
259
+ from trilogy.ai import Provider, text_to_query
260
+ import os
261
+
262
+ executor = Dialects.DUCK_DB.default_executor(
263
+ environment=Environment(working_path=Path(__file__).parent)
264
+ )
265
+
266
+ api_key = os.environ.get(OPENAI_API_KEY)
267
+ if not api_key:
268
+ raise ValueError("OPENAI_API_KEY required for gpt generation")
269
+ # load a model
270
+ executor.parse_file("flight.preql")
271
+ # create tables in the DB if needed
272
+ executor.execute_file("setup.sql")
273
+ # generate a query
274
+ query = text_to_query(
275
+ executor.environment,
276
+ "number of flights by month in 2005",
277
+ Provider.OPENAI,
278
+ "gpt-5-chat-latest",
279
+ api_key,
280
+ )
281
+
282
+ # print the generated trilogy query
283
+ print(query)
284
+ # run it
285
+ results = executor.execute_text(query)[-1].fetchall()
286
+ assert len(results) == 12
287
+
288
+ for row in results:
289
+ # all monthly flights are between 5000 and 7000
290
+ assert row[1] > 5000 and row[1] < 7000, row
291
+
292
+ ```
293
+
228
294
  ### CLI Usage
229
295
 
230
296
  Trilogy can be run through a CLI tool, also named 'trilogy'.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.107
3
+ Version: 0.0.3.109
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -28,6 +28,8 @@ Provides-Extra: bigquery
28
28
  Requires-Dist: sqlalchemy-bigquery; extra == "bigquery"
29
29
  Provides-Extra: snowflake
30
30
  Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
31
+ Provides-Extra: ai
32
+ Requires-Dist: httpx; extra == "ai"
31
33
  Dynamic: author-email
32
34
  Dynamic: classifier
33
35
  Dynamic: description
@@ -113,6 +115,31 @@ ORDER BY
113
115
  LIMIT 10;
114
116
  ```
115
117
 
118
+ ## Trilogy is Easy to Write
119
+ For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
120
+
121
+ (full code in the python API section.)
122
+
123
+ ```python
124
+ query = text_to_query(
125
+ executor.environment,
126
+ "number of flights by month in 2005",
127
+ Provider.OPENAI,
128
+ "gpt-5-chat-latest",
129
+ api_key,
130
+ )
131
+
132
+ # get a ready to run query
133
+ print(query)
134
+ # typical output
135
+ '''where local.dep_time.year = 2020
136
+ select
137
+ local.dep_time.month,
138
+ count(local.id2) as number_of_flights
139
+ order by
140
+ local.dep_time.month asc;'''
141
+ ```
142
+
116
143
  ## Goals
117
144
 
118
145
  Versus SQL, Trilogy aims to:
@@ -264,6 +291,47 @@ for row in results:
264
291
  print(x)
265
292
  ```
266
293
 
294
+ ### LLM Usage
295
+
296
+ Connect to your favorite provider and generate queries with confidence and high accuracy.
297
+
298
+ ```python
299
+ from trilogy import Environment, Dialects
300
+ from trilogy.ai import Provider, text_to_query
301
+ import os
302
+
303
+ executor = Dialects.DUCK_DB.default_executor(
304
+ environment=Environment(working_path=Path(__file__).parent)
305
+ )
306
+
307
+ api_key = os.environ.get(OPENAI_API_KEY)
308
+ if not api_key:
309
+ raise ValueError("OPENAI_API_KEY required for gpt generation")
310
+ # load a model
311
+ executor.parse_file("flight.preql")
312
+ # create tables in the DB if needed
313
+ executor.execute_file("setup.sql")
314
+ # generate a query
315
+ query = text_to_query(
316
+ executor.environment,
317
+ "number of flights by month in 2005",
318
+ Provider.OPENAI,
319
+ "gpt-5-chat-latest",
320
+ api_key,
321
+ )
322
+
323
+ # print the generated trilogy query
324
+ print(query)
325
+ # run it
326
+ results = executor.execute_text(query)[-1].fetchall()
327
+ assert len(results) == 12
328
+
329
+ for row in results:
330
+ # all monthly flights are between 5000 and 7000
331
+ assert row[1] > 5000 and row[1] < 7000, row
332
+
333
+ ```
334
+
267
335
  ### CLI Usage
268
336
 
269
337
  Trilogy can be run through a CLI tool, also named 'trilogy'.
@@ -44,6 +44,19 @@ trilogy/parser.py
44
44
  trilogy/py.typed
45
45
  trilogy/render.py
46
46
  trilogy/utility.py
47
+ trilogy/ai/__init__.py
48
+ trilogy/ai/constants.py
49
+ trilogy/ai/conversation.py
50
+ trilogy/ai/enums.py
51
+ trilogy/ai/execute.py
52
+ trilogy/ai/models.py
53
+ trilogy/ai/prompts.py
54
+ trilogy/ai/providers/__init__.py
55
+ trilogy/ai/providers/anthropic.py
56
+ trilogy/ai/providers/base.py
57
+ trilogy/ai/providers/google.py
58
+ trilogy/ai/providers/openai.py
59
+ trilogy/ai/providers/utils.py
47
60
  trilogy/authoring/__init__.py
48
61
  trilogy/core/__init__.py
49
62
  trilogy/core/constants.py
@@ -8,6 +8,9 @@ duckdb<1.4.0
8
8
  duckdb-engine
9
9
  click
10
10
 
11
+ [ai]
12
+ httpx
13
+
11
14
  [bigquery]
12
15
  sqlalchemy-bigquery
13
16
 
@@ -47,6 +47,7 @@ setuptools.setup(
47
47
  "postgres": ["psycopg2-binary"],
48
48
  "bigquery": ["sqlalchemy-bigquery"],
49
49
  "snowflake": ["snowflake-sqlalchemy"],
50
+ "ai": ["httpx"],
50
51
  },
51
52
  entry_points={
52
53
  "console_scripts": ["trilogy=trilogy.scripts.trilogy:cli"],
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.107"
7
+ __version__ = "0.0.3.109"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -0,0 +1,19 @@
1
+ from trilogy.ai.conversation import Conversation
2
+ from trilogy.ai.enums import Provider
3
+ from trilogy.ai.execute import text_to_query
4
+ from trilogy.ai.models import LLMMessage
5
+ from trilogy.ai.prompts import create_query_prompt
6
+ from trilogy.ai.providers.anthropic import AnthropicProvider
7
+ from trilogy.ai.providers.google import GoogleProvider
8
+ from trilogy.ai.providers.openai import OpenAIProvider
9
+
10
+ __all__ = [
11
+ "Conversation",
12
+ "LLMMessage",
13
+ "OpenAIProvider",
14
+ "GoogleProvider",
15
+ "AnthropicProvider",
16
+ "create_query_prompt",
17
+ "text_to_query",
18
+ "Provider",
19
+ ]
@@ -0,0 +1,92 @@
1
+ from trilogy.core.enums import FunctionClass, FunctionType
2
+ from trilogy.core.functions import FUNCTION_REGISTRY
3
+
4
+ RULE_PROMPT = """Trilogy statements define a semantic model or query. If a user is asking for data, they want a SELECT.
5
+ Semantic model statements:
6
+ - import <> imports a model to reuse. The output of imports will be visible in fields available to use.
7
+ - key|property|auto|metric defines fields locally. The output will also be visible in fields available to use, so you generally don't need to edit these unless requested.
8
+ - datasource statements define a datasource, which is a mapping of fields to a SQL database table. The left side is the SQL column name, the right side is the field name.
9
+
10
+ SELECT RULES:
11
+ - No FROM, JOIN, GROUP BY, SUB SELECTS, DISTINCT, UNION, or SELECT *.
12
+ - All fields exist in a global namespace; field paths look like `order.product.id`. Always use the full path. NEVER include a from clause.
13
+ - If a field has a grain defined, and that grain is not in the query output, aggregate it to get desired result.
14
+ - If a field has a 'alias_for' defined, it is shorthand for that calculation. Use the field name instead of the calculation in your query to be concise.
15
+ - Newly created fields at the output of the select must be aliased with as (e.g. `sum(births) as all_births`).
16
+ - Aliases cannot happen inside calculations or in the where/having/order clause. Never alias fields with existing names. 'sum(revenue) as total_revenue' is valid, but '(sum(births) as total_revenue) +1 as revenue_plus_one' is not.
17
+ - Implicit grouping: NEVER include a group by clause. Grouping is by non-aggregated fields in the SELECT clause.
18
+ - You can dynamically group inline to get groups at different grains - ex: `sum(metric) by dim1, dim2 as sum_by_dim1_dm2` for alternate grouping. If you are grouping a defined aggregate
19
+ - Count must specify a field (no `count(*)`) Counts are automatically deduplicated. Do not ever use DISTINCT.
20
+ - Since there are no underlying tables, sum/count of a constant should always specify a grain field (e.g. `sum(1) by x as count`).
21
+ - Aggregates in SELECT must be filtered via HAVING. Use WHERE for pre-aggregation filters.
22
+ - Use `field ? condition` for inline filters (e.g. `sum(x ? x > 0)`).
23
+ - Always use a reasonable `LIMIT` for final queries unless the request is for a time series or line chart.
24
+ - Window functions: `rank entity [optional over group] by field desc` (e.g. `rank name over state by sum(births) desc as top_name`) Do not use parentheses for over.
25
+ - Functions. All function names have parenthese (e.g. `sum(births)`, `date_part('year', dep_time)`). For no arguments, use empty parentheses (e.g. `current_date()`).
26
+ - For lag/lead, offset is first: lag/lead offset field order by expr asc/desc.
27
+ - For lag/lead with a window clause: lag/lead offset field by window_clause order by expr asc/desc.
28
+ - Use `::type` casting, e.g., `"2020-01-01"::date`.
29
+ - Date_parts have no quotes; use `date_part(order_date, year)` instead of `date_part(order_date, 'year')`.
30
+ - Comments use `#` only, per line.
31
+ - Two example queries: "where year between 1940 and 1950
32
+ select
33
+ name,
34
+ state,
35
+ sum(births) AS all_births,
36
+ sum(births ? state = 'VT') AS vermont_births,
37
+ rank name over state by all_births desc AS state_rank,
38
+ rank name by sum(births) by name desc AS all_rank
39
+ having
40
+ all_rank<11
41
+ and state = 'ID'
42
+ order by
43
+ all_rank asc
44
+ limit 5;", "where dep_time between '2002-01-01'::datetime and '2010-01-31'::datetime
45
+ select
46
+ carrier.name,
47
+ count(id2) AS total_flights,
48
+ total_flights / date_diff(min(dep_time.date), max(dep_time.date), DAY) AS average_daily_flights
49
+ order by
50
+ total_flights desc;"""
51
+
52
+
53
+ def render_function(function_type: FunctionType, example: str | None = None):
54
+ info = FUNCTION_REGISTRY[function_type]
55
+
56
+ if info.arg_count == -1:
57
+ # Infinite/variable number of arguments
58
+ base = f"{function_type.value}(<arg1>, <arg2>, ..., <argN>)"
59
+ elif info.arg_count == 0:
60
+ # No arguments
61
+ base = f"{function_type.value}()"
62
+ else:
63
+ # Fixed number of arguments
64
+ base = f"{function_type.value}({', '.join([f'<arg{p}>' for p in range(1, info.arg_count + 1)])})"
65
+
66
+ if example:
67
+ base += f" e.g. {example}"
68
+ return base
69
+
70
+
71
+ FUNCTION_EXAMPLES = {
72
+ FunctionType.DATE_ADD: "date_add('2020-01-01'::date, month, 1)",
73
+ FunctionType.DATE_DIFF: "date_diff('2020-01-01'::date, '2020-01-02'::date, day)",
74
+ FunctionType.DATE_PART: "date_part('2020-01-01'::date, year)",
75
+ FunctionType.DATE_SUB: "date_sub('2020-01-01'::date, day, 1)",
76
+ FunctionType.DATE_TRUNCATE: "date_trunc('2020-01-01'::date, month)",
77
+ FunctionType.CURRENT_TIMESTAMP: "now()",
78
+ }
79
+
80
+ FUNCTIONS = "\n".join(
81
+ [
82
+ render_function(v, example=FUNCTION_EXAMPLES.get(v))
83
+ for x, v in FunctionType.__members__.items()
84
+ if v in FUNCTION_REGISTRY
85
+ ]
86
+ )
87
+
88
+ AGGREGATE_FUNCTIONS = [
89
+ x
90
+ for x, info in FunctionType.__members__.items()
91
+ if x in FunctionClass.AGGREGATE_FUNCTIONS.value
92
+ ]
@@ -0,0 +1,99 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal, Union
3
+
4
+ from trilogy import Environment
5
+ from trilogy.ai.models import LLMMessage, LLMRequestOptions
6
+ from trilogy.ai.prompts import TRILOGY_LEAD_IN, create_query_prompt
7
+ from trilogy.ai.providers.base import LLMProvider
8
+ from trilogy.core.exceptions import (
9
+ InvalidSyntaxException,
10
+ NoDatasourceException,
11
+ UndefinedConceptException,
12
+ UnresolvableQueryException,
13
+ )
14
+ from trilogy.core.query_processor import process_query
15
+
16
+
17
+ @dataclass
18
+ class Conversation:
19
+
20
+ messages: list[LLMMessage]
21
+ provider: LLMProvider
22
+ id: str | None = None
23
+
24
+ @classmethod
25
+ def create(
26
+ cls,
27
+ provider: LLMProvider,
28
+ model_prompt: str = TRILOGY_LEAD_IN,
29
+ id: str | None = None,
30
+ ) -> "Conversation":
31
+ system_message = LLMMessage(role="system", content=model_prompt)
32
+ messages = [system_message]
33
+ return cls(id=id, messages=messages, provider=provider)
34
+
35
+ def add_message(
36
+ self,
37
+ message: Union[LLMMessage, str],
38
+ role: Literal["user", "assistant"] = "user",
39
+ ) -> None:
40
+ """
41
+ Add a message to the conversation.
42
+
43
+ Args:
44
+ message: Either an LLMMessage object or a string content
45
+ role: The role for the message if a string is provided (default: 'user')
46
+ """
47
+ if isinstance(message, str):
48
+ message = LLMMessage(role=role, content=message)
49
+ self.messages.append(message)
50
+
51
+ def get_response(self) -> LLMMessage:
52
+ options = LLMRequestOptions()
53
+ response = self.provider.generate_completion(options, history=self.messages)
54
+ response_message = LLMMessage(role="assistant", content=response.text)
55
+ self.add_message(response_message)
56
+ return response_message
57
+
58
+ def extract_response(self, content: str) -> str:
59
+ # get contents in triple backticks
60
+ content = content.replace('"""', "```")
61
+ if "```" in content:
62
+ parts = content.split("```")
63
+ if len(parts) >= 3:
64
+ return parts[1].strip()
65
+ return content
66
+
67
+ def generate_query(
68
+ self, user_input: str, environment: Environment, attempts: int = 4
69
+ ) -> str:
70
+ attempts = 0
71
+ self.add_message(create_query_prompt(user_input, environment), role="user")
72
+ e = None
73
+ while attempts < 4:
74
+ attempts += 1
75
+
76
+ response_message = self.get_response()
77
+ response = self.extract_response(response_message.content)
78
+ if not response.strip()[-1] == ";":
79
+ response += ";"
80
+ try:
81
+ env, raw = environment.parse(response)
82
+ process_query(statement=raw[-1], environment=environment)
83
+ return response
84
+ except (
85
+ InvalidSyntaxException,
86
+ NoDatasourceException,
87
+ UnresolvableQueryException,
88
+ UndefinedConceptException,
89
+ SyntaxError,
90
+ ) as e2:
91
+ e = e2
92
+ self.add_message(
93
+ f"The previous response could not be parsed due to the error: {str(e)}. Please generate a new query with the issues fixed. Use the same response format.",
94
+ role="user",
95
+ )
96
+
97
+ raise Exception(
98
+ f"Failed to generate a valid query after {attempts} attempts. Last error: {str(e)}. Full conversation: {self.messages}"
99
+ )
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Provider(Enum):
5
+ OPENAI = "openai"
6
+ ANTHROPIC = "anthropic"
7
+ GOOGLE = "google"
@@ -0,0 +1,50 @@
1
+ from trilogy import Environment
2
+ from trilogy.ai.conversation import Conversation
3
+ from trilogy.ai.enums import Provider
4
+ from trilogy.ai.providers.base import LLMProvider
5
+
6
+
7
+ def text_to_query(
8
+ environment: Environment,
9
+ user_input: str,
10
+ provider: Provider,
11
+ model: str,
12
+ secret: str | None = None,
13
+ ) -> str:
14
+ llm_provider: LLMProvider
15
+
16
+ if provider == Provider.OPENAI:
17
+ from trilogy.ai.providers.openai import OpenAIProvider
18
+
19
+ llm_provider = OpenAIProvider(
20
+ name="openai",
21
+ api_key=secret,
22
+ model=model,
23
+ )
24
+ elif provider == Provider.ANTHROPIC:
25
+ from trilogy.ai.providers.anthropic import AnthropicProvider
26
+
27
+ llm_provider = AnthropicProvider(
28
+ name="anthropic",
29
+ api_key=secret,
30
+ model=model,
31
+ )
32
+ elif provider == Provider.GOOGLE:
33
+ from trilogy.ai.providers.google import GoogleProvider
34
+
35
+ llm_provider = GoogleProvider(
36
+ name="google",
37
+ api_key=secret,
38
+ model=model,
39
+ )
40
+ else:
41
+ raise ValueError(f"Unsupported provider: {provider}")
42
+ conversation = Conversation.create(
43
+ provider=llm_provider,
44
+ )
45
+
46
+ response = conversation.generate_query(
47
+ user_input=user_input, environment=environment
48
+ )
49
+
50
+ return response
@@ -0,0 +1,34 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal, Optional
3
+
4
+
5
+ @dataclass
6
+ class UsageDict:
7
+ prompt_tokens: int
8
+ completion_tokens: int
9
+ total_tokens: int
10
+
11
+
12
+ @dataclass
13
+ class LLMResponse:
14
+ text: str
15
+ usage: UsageDict
16
+
17
+
18
+ @dataclass
19
+ class LLMRequestOptions:
20
+ max_tokens: Optional[int] = None
21
+ temperature: Optional[float] = None
22
+ top_p: Optional[float] = None
23
+
24
+
25
+ @dataclass
26
+ class LLMMessage:
27
+ role: Literal["user", "assistant", "system"]
28
+ content: str
29
+ model_info: Optional[dict] = None
30
+ hidden: bool = False # Used to hide messages in the UI
31
+
32
+ def __post_init__(self):
33
+ if self.model_info is None:
34
+ self.model_info = {}
@@ -0,0 +1,30 @@
1
+ from trilogy import Environment
2
+ from trilogy.ai.constants import AGGREGATE_FUNCTIONS, FUNCTIONS, RULE_PROMPT
3
+ from trilogy.authoring import Concept, DataType
4
+
5
+ TRILOGY_LEAD_IN = f'''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.
6
+
7
+ Key Trilogy Syntax Rules:
8
+ {RULE_PROMPT}
9
+
10
+ Aggregate Functions:
11
+ {AGGREGATE_FUNCTIONS}
12
+
13
+ Functions:
14
+ {FUNCTIONS}
15
+
16
+ Valid types:
17
+ {[x.value for x in DataType]}
18
+
19
+ 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}}"""
20
+ '''
21
+
22
+
23
+ def concepts_to_fields_prompt(concepts: list[Concept]) -> str:
24
+ return ", ".join([f"[name: {c.address} | type: {c.datatype}" for c in concepts])
25
+
26
+
27
+ def create_query_prompt(query: str, environment: Environment) -> str:
28
+ fields = concepts_to_fields_prompt(list(environment.concepts.values()))
29
+ return f'''
30
+ 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;"""'''