relationalai 0.12.8__py3-none-any.whl → 0.12.10__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.
Files changed (39) hide show
  1. relationalai/__init__.py +9 -0
  2. relationalai/clients/__init__.py +2 -2
  3. relationalai/clients/local.py +571 -0
  4. relationalai/clients/snowflake.py +106 -83
  5. relationalai/debugging.py +5 -2
  6. relationalai/semantics/__init__.py +2 -2
  7. relationalai/semantics/internal/__init__.py +2 -2
  8. relationalai/semantics/internal/internal.py +53 -14
  9. relationalai/semantics/lqp/README.md +34 -0
  10. relationalai/semantics/lqp/compiler.py +1 -1
  11. relationalai/semantics/lqp/constructors.py +7 -0
  12. relationalai/semantics/lqp/executor.py +35 -39
  13. relationalai/semantics/lqp/intrinsics.py +4 -3
  14. relationalai/semantics/lqp/ir.py +4 -0
  15. relationalai/semantics/lqp/model2lqp.py +47 -14
  16. relationalai/semantics/lqp/passes.py +7 -4
  17. relationalai/semantics/lqp/rewrite/__init__.py +4 -1
  18. relationalai/semantics/lqp/rewrite/annotate_constraints.py +55 -0
  19. relationalai/semantics/lqp/rewrite/extract_keys.py +22 -3
  20. relationalai/semantics/lqp/rewrite/function_annotations.py +91 -56
  21. relationalai/semantics/lqp/rewrite/functional_dependencies.py +314 -0
  22. relationalai/semantics/lqp/rewrite/quantify_vars.py +14 -0
  23. relationalai/semantics/lqp/validators.py +3 -0
  24. relationalai/semantics/metamodel/builtins.py +10 -0
  25. relationalai/semantics/metamodel/rewrite/extract_nested_logicals.py +5 -4
  26. relationalai/semantics/metamodel/rewrite/flatten.py +10 -4
  27. relationalai/semantics/metamodel/typer/typer.py +13 -0
  28. relationalai/semantics/metamodel/types.py +2 -1
  29. relationalai/semantics/reasoners/graph/core.py +44 -53
  30. relationalai/semantics/rel/compiler.py +19 -1
  31. relationalai/semantics/tests/test_snapshot_abstract.py +3 -0
  32. relationalai/tools/debugger.py +4 -2
  33. relationalai/tools/qb_debugger.py +5 -3
  34. relationalai/util/otel_handler.py +10 -4
  35. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/METADATA +2 -2
  36. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/RECORD +39 -35
  37. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/WHEEL +0 -0
  38. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/entry_points.txt +0 -0
  39. {relationalai-0.12.8.dist-info → relationalai-0.12.10.dist-info}/licenses/LICENSE +0 -0
@@ -20,7 +20,7 @@ from relationalai.semantics import (
20
20
  count, sum, avg,
21
21
  )
22
22
  from relationalai.docutils import include_in_docs
23
- from relationalai.semantics.internal import annotations
23
+ from relationalai.semantics.internal import annotations, AnyEntity
24
24
  from relationalai.semantics.internal import internal as builder_internal # For primitive graph algorithms.
25
25
  from relationalai.semantics.std.math import abs, isnan, isinf, maximum, natural_log, sqrt
26
26
  from relationalai.semantics.std.integers import int64
@@ -158,7 +158,7 @@ class Graph():
158
158
  f"but is a `{type(weighted).__name__}`."
159
159
  )
160
160
  assert isinstance(model, Model), (
161
- "The `model` argument must be a `builder.Model`, "
161
+ "The `model` argument must be a `relationalai.semantics.Model`, "
162
162
  f"but is a `{type(model).__name__}`."
163
163
  )
164
164
  self.directed = directed
@@ -355,7 +355,7 @@ class Graph():
355
355
  @cached_property
356
356
  def Node(self) -> Concept:
357
357
  """Lazily define and cache the self.Node concept."""
358
- _Node = self._user_node_concept or self._model.Concept(self._NodeConceptStr)
358
+ _Node = self._user_node_concept or self._model.Concept(self._NodeConceptStr, extends=[AnyEntity])
359
359
  _Node.annotate(annotations.track("graphs", "Node"))
360
360
  return _Node
361
361
 
@@ -2304,14 +2304,14 @@ class Graph():
2304
2304
  # neighbor_a_rel = self._neighbor_of(node_subset_from)
2305
2305
  #
2306
2306
  # domain_w = Relationship(f"{{node:{self._NodeConceptStr}}} is the domain of `w` in `common_neighbor(u, v, w)`")
2307
- # node_x, node_y = graph.Node.ref(), graph.Node.ref()
2308
- # where(neighbor_a_rel(node_x, node_y)).define(domain_w(node_y))
2307
+ # where(neighbor_a_rel(node_a, node_b)).define(domain_w(node_b))
2309
2308
  # neighbor_b_rel = self._neighbor_of(domain_w)
2310
2309
  #
2311
2310
  # node_constraint = []
2312
2311
  #
2313
- # # need to reverse the args of `neighbor_b_rel()`, due to its domain constraint
2314
- # # relies on the symmetry of `neighbor`
2312
+ # # For this case only, we reverse the args of `neighbor_b_rel()`, which
2313
+ # # is allowed by the symmetry of `neighbor`, in order to take advantage
2314
+ # # of domain constraint on `neighbor_b_rel()`.
2315
2315
  # where(
2316
2316
  # *node_constraint,
2317
2317
  # neighbor_a_rel(node_a, neighbor_node),
@@ -2748,15 +2748,15 @@ class Graph():
2748
2748
  if node_subset is None:
2749
2749
  # No constraint - use cached count_inneighbor relationship and all nodes
2750
2750
  count_inneighbor_rel = self._count_inneighbor
2751
- node_set = self.Node
2751
+ node_constraint = []
2752
2752
  else:
2753
2753
  # Constrained to nodes in the subset - use constrained count_inneighbor relationship
2754
2754
  count_inneighbor_rel = self._count_inneighbor_of(node_subset)
2755
- node_set = node_subset
2755
+ node_constraint = [node_subset(self.Node)]
2756
2756
 
2757
2757
  # Apply the same indegree logic for both cases
2758
2758
  where(
2759
- node_set(self.Node),
2759
+ *node_constraint,
2760
2760
  _indegree := where(count_inneighbor_rel(self.Node, Integer)).select(Integer) | 0,
2761
2761
  ).define(_indegree_rel(self.Node, _indegree))
2762
2762
 
@@ -2933,15 +2933,15 @@ class Graph():
2933
2933
  if node_subset is None:
2934
2934
  # No constraint - use cached count_outneighbor relationship and all nodes
2935
2935
  count_outneighbor_rel = self._count_outneighbor
2936
- node_set = self.Node
2936
+ node_constraint = []
2937
2937
  else:
2938
2938
  # Constrained to nodes in the subset - use constrained count_outneighbor relationship
2939
2939
  count_outneighbor_rel = self._count_outneighbor_of(node_subset)
2940
- node_set = node_subset
2940
+ node_constraint = [node_subset(self.Node)]
2941
2941
 
2942
2942
  # Apply the same outdegree logic for both cases
2943
2943
  where(
2944
- node_set(self.Node),
2944
+ *node_constraint,
2945
2945
  _outdegree := where(count_outneighbor_rel(self.Node, Integer)).select(Integer) | 0,
2946
2946
  ).define(_outdegree_rel(self.Node, _outdegree))
2947
2947
 
@@ -3099,12 +3099,12 @@ class Graph():
3099
3099
  node, neighbor, weight = self.Node.ref(), self.Node.ref(), Float.ref()
3100
3100
 
3101
3101
  if node_subset is None:
3102
- node_constraint = node # No constraint on nodes.
3102
+ node_constraint = [] # No constraint on nodes.
3103
3103
  else:
3104
- node_constraint = node_subset(node) # Nodes constrained to given subset.
3104
+ node_constraint = [node_subset(node)] # Nodes constrained to given subset.
3105
3105
 
3106
3106
  where(
3107
- node_constraint,
3107
+ *node_constraint,
3108
3108
  weighted_degree_no_loops := sum(neighbor, weight).per(node).where(
3109
3109
  self._weight(node, neighbor, weight),
3110
3110
  node != neighbor,
@@ -3172,7 +3172,7 @@ class Graph():
3172
3172
  >>> define(n1, n2, n3)
3173
3173
  >>> define(
3174
3174
  ... Edge.new(src=n1, dst=n2, weight=1.0),
3175
- ... Edge.new(src=n2, dst=n1, weight=-1.0),
3175
+ ... Edge.new(src=n2, dst=n1, weight=0.0),
3176
3176
  ... Edge.new(src=n2, dst=n3, weight=1.0),
3177
3177
  ... )
3178
3178
  >>>
@@ -3186,7 +3186,7 @@ class Graph():
3186
3186
  ... ).inspect()
3187
3187
  ▰▰▰▰ Setup complete
3188
3188
  id node_weighted_indegree
3189
- 0 1 -1.0
3189
+ 0 1 0.0
3190
3190
  1 2 1.0
3191
3191
  2 3 1.0
3192
3192
  >>>
@@ -3218,9 +3218,6 @@ class Graph():
3218
3218
  weighted_outdegree
3219
3219
 
3220
3220
  """
3221
- # TODO: It looks like the weights in the example in the docstring above
3222
- # are holdovers from a version of the library that did not disallow
3223
- # negative weights. Need to update the example to use only non-negative weights.
3224
3221
  if of is None:
3225
3222
  return self._weighted_indegree
3226
3223
  else:
@@ -3251,20 +3248,15 @@ class Graph():
3251
3248
  # Choose the appropriate node set
3252
3249
  if node_subset is None:
3253
3250
  # No constraint - use all nodes
3254
- node_set = self.Node
3251
+ node_constraint = []
3255
3252
  else:
3256
3253
  # Constrained to nodes in the subset
3257
- node_set = node_subset
3258
- # TODO: In a future cleanup pass, replace `node_set` with a `node_constraint`
3259
- # that replaces the `node_set(self.Node)` in the where clause below,
3260
- # and generates only `self.Node` (rather than `self.Node(self.Node)`)
3261
- # in the `subset is None` case. This applies to a couple other
3262
- # degree-of type relations as well.
3254
+ node_constraint = [node_subset(self.Node)]
3263
3255
 
3264
3256
  # Apply the weighted indegree logic for both cases
3265
3257
  src, inweight = self.Node.ref(), Float.ref()
3266
3258
  where(
3267
- node_set(self.Node),
3259
+ *node_constraint,
3268
3260
  _weighted_indegree := sum(src, inweight).per(self.Node).where(self._weight(src, self.Node, inweight)) | 0.0,
3269
3261
  ).define(_weighted_indegree_rel(self.Node, _weighted_indegree))
3270
3262
 
@@ -3324,7 +3316,7 @@ class Graph():
3324
3316
  >>> define(n1, n2, n3)
3325
3317
  >>> define(
3326
3318
  ... Edge.new(src=n1, dst=n2, weight=1.0),
3327
- ... Edge.new(src=n2, dst=n1, weight=-1.0),
3319
+ ... Edge.new(src=n2, dst=n1, weight=0.0),
3328
3320
  ... Edge.new(src=n2, dst=n3, weight=1.0),
3329
3321
  ... )
3330
3322
  >>>
@@ -3339,7 +3331,7 @@ class Graph():
3339
3331
  ▰▰▰▰ Setup complete
3340
3332
  id node_weighted_outdegree
3341
3333
  0 1 1.0
3342
- 1 2 0.0
3334
+ 1 2 1.0
3343
3335
  2 3 0.0
3344
3336
  >>>
3345
3337
  >>> # 4. Use 'of' parameter to constrain the set of nodes to compute weighted outdegree of
@@ -3355,7 +3347,7 @@ class Graph():
3355
3347
  ▰▰▰▰ Setup complete
3356
3348
  id node_weighted_outdegree
3357
3349
  0 1 1.0
3358
- 1 2 0.0
3350
+ 1 2 1.0
3359
3351
 
3360
3352
  Notes
3361
3353
  -----
@@ -3404,15 +3396,15 @@ class Graph():
3404
3396
  # Choose the appropriate node set
3405
3397
  if node_subset is None:
3406
3398
  # No constraint - use all nodes
3407
- node_set = self.Node
3399
+ node_constraint = []
3408
3400
  else:
3409
3401
  # Constrained to nodes in the subset
3410
- node_set = node_subset
3402
+ node_constraint = [node_subset(self.Node)]
3411
3403
 
3412
3404
  # Apply the weighted outdegree logic for both cases
3413
3405
  dst, outweight = self.Node.ref(), Float.ref()
3414
3406
  where(
3415
- node_set(self.Node),
3407
+ *node_constraint,
3416
3408
  _weighted_outdegree := sum(dst, outweight).per(self.Node).where(self._weight(self.Node, dst, outweight)) | 0.0,
3417
3409
  ).define(_weighted_outdegree_rel(self.Node, _weighted_outdegree))
3418
3410
 
@@ -4537,12 +4529,12 @@ class Graph():
4537
4529
  _triangle_count_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} belongs to {{count:Integer}} triangles")
4538
4530
 
4539
4531
  if node_subset is None:
4540
- node_constraint = self.Node # No constraint on nodes.
4532
+ node_constraint = [] # No constraint on nodes.
4541
4533
  else:
4542
- node_constraint = node_subset(self.Node) # Nodes constrained to given subset.
4534
+ node_constraint = [node_subset(self.Node)] # Nodes constrained to given subset.
4543
4535
 
4544
4536
  where(
4545
- node_constraint,
4537
+ *node_constraint,
4546
4538
  _count := self._nonzero_triangle_count_fragment(self.Node) | 0
4547
4539
  ).define(_triangle_count_rel(self.Node, _count))
4548
4540
 
@@ -4819,16 +4811,16 @@ class Graph():
4819
4811
  if node_subset is None:
4820
4812
  degree_no_self_rel = self._degree_no_self
4821
4813
  triangle_count_rel = self._triangle_count
4822
- node_constraint = node # No constraint on nodes.
4814
+ node_constraint = [] # No constraint on nodes.
4823
4815
  else:
4824
4816
  degree_no_self_rel = self._degree_no_self_of(node_subset)
4825
4817
  triangle_count_rel = self._triangle_count_of(node_subset)
4826
- node_constraint = node_subset(node) # Nodes constrained to given subset.
4818
+ node_constraint = [node_subset(node)] # Nodes constrained to given subset.
4827
4819
 
4828
4820
  degree_no_self = Integer.ref()
4829
4821
  triangle_count = Integer.ref()
4830
4822
  where(
4831
- node_constraint,
4823
+ *node_constraint,
4832
4824
  _lcc := where(
4833
4825
  degree_no_self_rel(node, degree_no_self),
4834
4826
  triangle_count_rel(node, triangle_count),
@@ -4866,12 +4858,12 @@ class Graph():
4866
4858
  node, neighbor = self.Node.ref(), self.Node.ref()
4867
4859
 
4868
4860
  if node_subset is None:
4869
- node_constraint = node # No constraint on nodes.
4861
+ node_constraint = [] # No constraint on nodes.
4870
4862
  else:
4871
- node_constraint = node_subset(node) # Nodes constrained to given subset.
4863
+ node_constraint = [node_subset(node)] # Nodes constrained to given subset.
4872
4864
 
4873
4865
  where(
4874
- node_constraint,
4866
+ *node_constraint,
4875
4867
  _dns := count(neighbor).per(node).where(self._no_loop_edge(node, neighbor)) | 0,
4876
4868
  ).define(_degree_no_self_rel(node, _dns))
4877
4869
 
@@ -7296,7 +7288,7 @@ class Graph():
7296
7288
 
7297
7289
  # TODO: Optimization opportunity. In some of the cases below
7298
7290
  # (unweighted in particular), the node_constraint is redundant with
7299
- # the constraints baked into the _count_outneigherbor_of and
7291
+ # the constraints baked into the _count_outneighbor_of and
7300
7292
  # _outneighbor_of relationships. The join with node_constraint
7301
7293
  # could be eliminated in those cases. Possibly also relevant to
7302
7294
  # other domain-constrained relations.
@@ -7366,19 +7358,18 @@ class Graph():
7366
7358
  # Define cosine similarity logic for both weighted and unweighted cases.
7367
7359
  if not self.weighted:
7368
7360
  # Unweighted case: use count of common outneighbors.
7369
- count_outneighor_u, count_outneighor_v = Integer.ref(), Integer.ref()
7361
+ count_outneighbor_u, count_outneighbor_v = Integer.ref(), Integer.ref()
7370
7362
  common_outneighbor_node = self.Node.ref()
7371
- score = Float.ref()
7372
7363
 
7373
7364
  where(
7374
7365
  *node_constraints,
7375
- count_outneighbor_u_rel(node_u, count_outneighor_u),
7376
- count_outneighbor_v_rel(node_v, count_outneighor_v),
7366
+ count_outneighbor_u_rel(node_u, count_outneighbor_u),
7367
+ count_outneighbor_v_rel(node_v, count_outneighbor_v),
7377
7368
  c_common := count(common_outneighbor_node).per(node_u, node_v).where(
7378
7369
  outneighbor_u_rel(node_u, common_outneighbor_node),
7379
7370
  outneighbor_v_rel(node_v, common_outneighbor_node),
7380
7371
  ),
7381
- score := c_common / sqrt(count_outneighor_u * count_outneighor_v),
7372
+ score := c_common / sqrt(count_outneighbor_u * count_outneighbor_v),
7382
7373
  ).define(
7383
7374
  _cosine_similarity_rel(node_u, node_v, score)
7384
7375
  )
@@ -8186,13 +8177,13 @@ class Graph():
8186
8177
  neighbor_node = self.Node.ref()
8187
8178
  if node_subset is not None:
8188
8179
  neighbor_rel = self._neighbor_of(node_subset)
8189
- node_constraint = node_subset(self.Node)
8180
+ node_constraint = [node_subset(self.Node)]
8190
8181
  else:
8191
8182
  neighbor_rel = self._neighbor
8192
- node_constraint = self.Node
8183
+ node_constraint = []
8193
8184
 
8194
8185
  where(
8195
- node_constraint,
8186
+ *node_constraint,
8196
8187
  not_(neighbor_rel(self.Node, neighbor_node))
8197
8188
  ).define(_isolated_node_rel(self.Node))
8198
8189
 
@@ -12,7 +12,7 @@ from relationalai.semantics.metamodel.util import OrderedSet, group_by, NameCach
12
12
  from relationalai.semantics.rel import rel, rel_utils as u, builtins as rel_bt
13
13
 
14
14
  from ..metamodel.rewrite import (Flatten, ExtractNestedLogicals, DNFUnionSplitter, DischargeConstraints, FormatOutputs)
15
- from ..lqp.rewrite import CDC, ExtractCommon, ExtractKeys, FunctionAnnotations, QuantifyVars, Splinter
15
+ from ..lqp.rewrite import CDC, ExtractCommon, ExtractKeys, FunctionAnnotations, QuantifyVars, Splinter, SplitMultiCheckRequires
16
16
 
17
17
  import math
18
18
 
@@ -24,6 +24,7 @@ import math
24
24
  class Compiler(c.Compiler):
25
25
  def __init__(self):
26
26
  super().__init__([
27
+ SplitMultiCheckRequires(),
27
28
  FunctionAnnotations(),
28
29
  DischargeConstraints(),
29
30
  Checker(),
@@ -696,7 +697,24 @@ class ModelToRel:
696
697
  rel_annos = cast(Tuple[rel.Annotation, ...], self.handle_list(filtered_annos))
697
698
  return rel_annos
698
699
 
700
+ # standard handling mistreats the integer arg of ranked `@function(:checked, k)`
701
+ def handle_ranked_function_annotation(self, n: ir.Annotation):
702
+ assert n.relation == bt.function_ranked and len(n.args) == 2
703
+ checked_lit = n.args[0]
704
+ rank_lit = n.args[1]
705
+ assert isinstance(checked_lit, ir.Literal) and isinstance(rank_lit, ir.Literal)
706
+ checked = rel.MetaValue(checked_lit.value)
707
+ rank = rank_lit.value
708
+ return rel.Annotation(
709
+ n.relation.name,
710
+ (checked, rank)
711
+ )
712
+
699
713
  def handle_annotation(self, n: ir.Annotation):
714
+ # special treatment for (ranked) @function(:checked, k)
715
+ if n.relation == bt.function_ranked:
716
+ return self.handle_ranked_function_annotation(n)
717
+
700
718
  # we know that annotations won't have vars, so we can ignore that type warning
701
719
  return rel.Annotation(
702
720
  n.relation.name,
@@ -2,6 +2,7 @@ import logging
2
2
  import os
3
3
  import uuid
4
4
  import sys
5
+ import datetime
5
6
  from abc import abstractmethod, ABC
6
7
 
7
8
  from relationalai.semantics.tests.logging import Capturer
@@ -47,6 +48,8 @@ class AbstractSnapshotTest(ABC):
47
48
  'use_sql': use_sql,
48
49
  'reasoner.rule.use_lqp': use_lqp,
49
50
  'keep_model': False,
51
+ # fix the current time to keep snapshots stable
52
+ 'datetime_now': datetime.datetime.fromisoformat("2025-12-01T12:00:00+00:00"),
50
53
  }
51
54
  if use_direct_access:
52
55
  # for direct access we can not use user/password authentication
@@ -3,6 +3,8 @@ import re
3
3
  from nicegui import ui
4
4
  import json
5
5
 
6
+ from relationalai.debugging import DEBUG_LOG_FILE
7
+
6
8
 
7
9
  last_mod_time = None
8
10
  current_json_objects = []
@@ -141,12 +143,12 @@ def poll():
141
143
  global current_json_objects
142
144
  # Check the last modification time of the file
143
145
  try:
144
- mod_time = os.path.getmtime('debug.jsonl')
146
+ mod_time = os.path.getmtime(DEBUG_LOG_FILE)
145
147
  if last_mod_time is None or mod_time > last_mod_time:
146
148
  last_mod_time = mod_time
147
149
  # File has changed, read and parse the new content
148
150
  new_objects = []
149
- with open('debug.jsonl', 'r') as file:
151
+ with open(DEBUG_LOG_FILE, 'r') as file:
150
152
  for line in file:
151
153
  try:
152
154
  # Parse each JSON object and add it to the list
@@ -3,6 +3,8 @@ import re
3
3
  from nicegui import ui
4
4
  import json
5
5
 
6
+ from relationalai.debugging import DEBUG_LOG_FILE
7
+
6
8
  #--------------------------------------------------
7
9
  # Terminal nodes
8
10
  #--------------------------------------------------
@@ -12,7 +14,7 @@ TERMINAL_NODES = [
12
14
  ]
13
15
 
14
16
  #--------------------------------------------------
15
- # debug.jsonl helpers
17
+ # Debug log helpers
16
18
  #--------------------------------------------------
17
19
 
18
20
  class SpanNode:
@@ -344,11 +346,11 @@ def poll():
344
346
  global current_json_objects
345
347
  # Check the last modification time of the file
346
348
  try:
347
- mod_time = os.path.getmtime('debug.jsonl')
349
+ mod_time = os.path.getmtime(DEBUG_LOG_FILE)
348
350
  if last_mod_time is None or mod_time > last_mod_time:
349
351
  last_mod_time = mod_time
350
352
  # File has changed, read and parse the new content
351
- with open('debug.jsonl', 'r') as file:
353
+ with open(DEBUG_LOG_FILE, 'r') as file:
352
354
  content = file.read()
353
355
  if content:
354
356
  new_tree = parse_jsonl_to_tree(content)
@@ -20,6 +20,7 @@ from relationalai.debugging import logger, Span, filter_span_attrs, otel_traceid
20
20
  from relationalai.clients.snowflake import Resources
21
21
 
22
22
  MAX_PAYLOAD_SIZE = 25*1024 # 25KB
23
+ MAX_ATTRIBUTE_LENGTH = 1000
23
24
  _otel_initialized = False
24
25
 
25
26
 
@@ -44,8 +45,6 @@ class CachedSpanExporter(SpanExporter):
44
45
 
45
46
  def get_spans(self):
46
47
  return self.spans
47
-
48
-
49
48
  class NativeAppSpanExporter(SpanExporter):
50
49
  def __init__(self, resource:Resources, app_name:str):
51
50
  self.resource = resource
@@ -252,6 +251,12 @@ def encode_otlp_attribute(k, v):
252
251
  try:
253
252
  valueObj = {}
254
253
  if isinstance(v, str):
254
+ # Truncate metamodel attribute value if it's too large
255
+ if k == "metamodel" and len(v) > MAX_ATTRIBUTE_LENGTH:
256
+ original_length = len(v)
257
+ v = v[:MAX_ATTRIBUTE_LENGTH] + f"... (truncated from {original_length} chars)"
258
+ logging.warning(f"{k} attribute truncated from {original_length} to {MAX_ATTRIBUTE_LENGTH} characters")
259
+
255
260
  valueObj = {
256
261
  "stringValue": html.escape(v).replace("\n", "; ")
257
262
  }
@@ -369,7 +374,7 @@ def encode_spans_to_otlp_json(spans: Sequence[ReadableSpan], max_bytes: int) ->
369
374
  except Exception as e:
370
375
  logging.warning(f"Error encoding resource to OTLP JSON: {e}")
371
376
  return "", spans # Return the unencoded spans if resource encoding fails
372
-
377
+
373
378
  # TODO encode pyrel version for real
374
379
  header_str = f'{{"resourceSpans": [{{"resource":{resource_str}, "scopeSpans": [{{"scope":{{"name":"pyrel", "version":"v0.4.0"}},"spans":['
375
380
  footer_str = ']}]}]}'
@@ -378,7 +383,7 @@ def encode_spans_to_otlp_json(spans: Sequence[ReadableSpan], max_bytes: int) ->
378
383
  encoded_spans = []
379
384
  for span in spans:
380
385
  try:
381
- span_str = encode_span_to_otlp_json(span)
386
+ span_str = encode_span_to_otlp_json(span)
382
387
  except Exception as e:
383
388
  logging.warning(f"Error encoding span {span}: {e}")
384
389
  continue # Skip this span if encoding fails
@@ -460,6 +465,7 @@ def enable_otel_export(client_resources: Resources, app_name):
460
465
  if _otel_initialized:
461
466
  logging.warning("OTel already initialized, skipping.")
462
467
  return
468
+
463
469
  _otel_initialized = True
464
470
 
465
471
  if TRACE_PROVIDER is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: relationalai
3
- Version: 0.12.8
3
+ Version: 0.12.10
4
4
  Summary: RelationalAI Library and CLI
5
5
  Author-email: RelationalAI <support@relational.ai>
6
6
  License-File: LICENSE
@@ -11,7 +11,7 @@ Requires-Dist: colorama
11
11
  Requires-Dist: cryptography
12
12
  Requires-Dist: gravis
13
13
  Requires-Dist: inquirerpy
14
- Requires-Dist: lqp==0.1.18
14
+ Requires-Dist: lqp==0.1.19
15
15
  Requires-Dist: nicegui==2.16.1
16
16
  Requires-Dist: numpy<2
17
17
  Requires-Dist: opentelemetry-api