agent0-sdk 1.0.1__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -160,7 +160,7 @@ class IPFSClient:
160
160
  # add_str returns the CID directly as a string
161
161
  return result if isinstance(result, str) else result['Hash']
162
162
 
163
- def _pin_to_pinata(self, data: str) -> str:
163
+ def _pin_to_pinata(self, data: str, file_name: str = "file.json") -> str:
164
164
  """Pin data to Pinata using JWT authentication with v3 API."""
165
165
  import requests
166
166
  import tempfile
@@ -185,7 +185,7 @@ class IPFSClient:
185
185
  # Prepare the file for upload with public network setting
186
186
  with open(temp_path, 'rb') as file:
187
187
  files = {
188
- 'file': ('registration.json', file, 'application/json')
188
+ 'file': (file_name, file, 'application/json')
189
189
  }
190
190
 
191
191
  # Add network parameter to make file public
@@ -233,8 +233,9 @@ class IPFSClient:
233
233
 
234
234
  def add(self, data: str, **kwargs) -> str:
235
235
  """Add data to IPFS and return CID."""
236
+ file_name = kwargs.pop("file_name", None)
236
237
  if self.pinata_enabled:
237
- return self._pin_to_pinata(data)
238
+ return self._pin_to_pinata(data, file_name=file_name or "file.json")
238
239
  elif self.filecoin_pin_enabled:
239
240
  # Create temporary file for Filecoin Pin
240
241
  with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
@@ -251,11 +252,12 @@ class IPFSClient:
251
252
 
252
253
  def add_file(self, filepath: str, **kwargs) -> str:
253
254
  """Add file to IPFS and return CID."""
255
+ file_name = kwargs.pop("file_name", None)
254
256
  if self.pinata_enabled:
255
257
  # Read file and send to Pinata
256
258
  with open(filepath, 'r') as f:
257
259
  data = f.read()
258
- return self._pin_to_pinata(data)
260
+ return self._pin_to_pinata(data, file_name=file_name or "file.json")
259
261
  elif self.filecoin_pin_enabled:
260
262
  return self._pin_to_filecoin(filepath)
261
263
  else:
@@ -332,7 +334,7 @@ class IPFSClient:
332
334
  def addRegistrationFile(self, registrationFile: "RegistrationFile", chainId: Optional[int] = None, identityRegistryAddress: Optional[str] = None, **kwargs) -> str:
333
335
  """Add registration file to IPFS and return CID."""
334
336
  data = registrationFile.to_dict(chain_id=chainId, identity_registry_address=identityRegistryAddress)
335
- return self.add_json(data, **kwargs)
337
+ return self.add_json(data, file_name="agent-registration.json", **kwargs)
336
338
 
337
339
  def getRegistrationFile(self, cid: str) -> "RegistrationFile":
338
340
  """Get registration file from IPFS by CID."""
@@ -342,7 +344,7 @@ class IPFSClient:
342
344
 
343
345
  def addFeedbackFile(self, feedbackData: Dict[str, Any], **kwargs) -> str:
344
346
  """Add feedback file to IPFS and return CID."""
345
- return self.add_json(feedbackData, **kwargs)
347
+ return self.add_json(feedbackData, file_name="feedback.json", **kwargs)
346
348
 
347
349
  def getFeedbackFile(self, cid: str) -> Dict[str, Any]:
348
350
  """Get feedback file from IPFS by CID."""
agent0_sdk/core/models.py CHANGED
@@ -185,7 +185,9 @@ class Feedback:
185
185
  id: tuple # (agentId, clientAddress, feedbackIndex) - tuple for efficiency
186
186
  agentId: AgentId
187
187
  reviewer: Address
188
- score: Optional[int] # 0-100
188
+ # ReputationRegistry Jan 2026: decimal value computed as (value:int256 / 10^valueDecimals).
189
+ # SDK exposes ONLY the computed value.
190
+ value: Optional[float]
189
191
  tags: List[str] = field(default_factory=list)
190
192
  text: Optional[str] = None
191
193
  context: Optional[Dict[str, Any]] = None
@@ -291,8 +293,8 @@ class SearchFeedbackParams:
291
293
  tasks: Optional[List[str]] = None
292
294
  names: Optional[List[str]] = None # MCP tool/resource/prompt names
293
295
  endpoint: Optional[str] = None # Filter by endpoint URI
294
- minScore: Optional[int] = None # 0-100
295
- maxScore: Optional[int] = None # 0-100
296
+ minValue: Optional[float] = None
297
+ maxValue: Optional[float] = None
296
298
  includeRevoked: bool = False
297
299
 
298
300
  def to_dict(self) -> Dict[str, Any]:
agent0_sdk/core/sdk.py CHANGED
@@ -491,111 +491,8 @@ class SDK:
491
491
 
492
492
  return self.indexer.search_agents(params, sort, page_size, cursor)
493
493
 
494
- # Feedback methods
495
- def prepareFeedback(
496
- self,
497
- agentId: AgentId,
498
- score: Optional[int] = None, # 0-100
499
- tags: List[str] = None,
500
- text: Optional[str] = None,
501
- capability: Optional[str] = None,
502
- name: Optional[str] = None,
503
- skill: Optional[str] = None,
504
- task: Optional[str] = None,
505
- context: Optional[Dict[str, Any]] = None,
506
- proofOfPayment: Optional[Dict[str, Any]] = None,
507
- extra: Optional[Dict[str, Any]] = None,
508
- ) -> Dict[str, Any]:
509
- """Prepare feedback file (local file/object)."""
510
- return self.feedback_manager.prepareFeedback(
511
- agentId=agentId,
512
- score=score,
513
- tags=tags,
514
- text=text,
515
- capability=capability,
516
- name=name,
517
- skill=skill,
518
- task=task,
519
- context=context,
520
- proofOfPayment=proofOfPayment,
521
- extra=extra
522
- )
523
-
524
- def giveFeedback(
525
- self,
526
- agentId: AgentId,
527
- feedbackFile: Dict[str, Any],
528
- idem: Optional[IdemKey] = None,
529
- feedback_auth: Optional[bytes] = None,
530
- ) -> Feedback:
531
- """Give feedback (maps 8004 endpoint)."""
532
- return self.feedback_manager.giveFeedback(
533
- agentId=agentId,
534
- feedbackFile=feedbackFile,
535
- idem=idem,
536
- feedback_auth=feedback_auth
537
- )
538
-
539
- def getFeedback(self, feedbackId: str) -> Feedback:
540
- """Get single feedback by ID string."""
541
- # Parse feedback ID
542
- agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
543
- return self.feedback_manager.getFeedback(agentId, clientAddress, feedbackIndex)
544
-
545
- def searchFeedback(
546
- self,
547
- agentId: AgentId,
548
- reviewers: Optional[List[Address]] = None,
549
- tags: Optional[List[str]] = None,
550
- capabilities: Optional[List[str]] = None,
551
- skills: Optional[List[str]] = None,
552
- tasks: Optional[List[str]] = None,
553
- names: Optional[List[str]] = None,
554
- minScore: Optional[int] = None,
555
- maxScore: Optional[int] = None,
556
- include_revoked: bool = False,
557
- first: int = 100,
558
- skip: int = 0,
559
- ) -> List[Feedback]:
560
- """Search feedback for an agent."""
561
- return self.feedback_manager.searchFeedback(
562
- agentId=agentId,
563
- clientAddresses=reviewers,
564
- tags=tags,
565
- capabilities=capabilities,
566
- skills=skills,
567
- tasks=tasks,
568
- names=names,
569
- minScore=minScore,
570
- maxScore=maxScore,
571
- include_revoked=include_revoked,
572
- first=first,
573
- skip=skip
574
- )
575
-
576
- def revokeFeedback(
577
- self,
578
- feedbackId: str,
579
- reason: Optional[str] = None,
580
- idem: Optional[IdemKey] = None,
581
- ) -> Dict[str, Any]:
582
- """Revoke feedback."""
583
- # Parse feedback ID
584
- agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
585
- return self.feedback_manager.revokeFeedback(agentId, feedbackIndex)
586
-
587
- def appendResponse(
588
- self,
589
- feedbackId: str,
590
- response: Dict[str, Any],
591
- idem: Optional[IdemKey] = None,
592
- ) -> Feedback:
593
- """Append a response/follow-up to existing feedback."""
594
- # Parse feedback ID
595
- agentId, clientAddress, feedbackIndex = Feedback.from_id_string(feedbackId)
596
- return self.feedback_manager.appendResponse(agentId, clientAddress, feedbackIndex, response)
494
+ # Feedback methods are defined later in this class (single authoritative API).
597
495
 
598
-
599
496
  def searchAgentsByReputation(
600
497
  self,
601
498
  agents: Optional[List[AgentId]] = None,
@@ -605,7 +502,7 @@ class SDK:
605
502
  skills: Optional[List[str]] = None,
606
503
  tasks: Optional[List[str]] = None,
607
504
  names: Optional[List[str]] = None,
608
- minAverageScore: Optional[int] = None, # 0-100
505
+ minAverageValue: Optional[float] = None,
609
506
  includeRevoked: bool = False,
610
507
  page_size: int = 50,
611
508
  cursor: Optional[str] = None,
@@ -625,7 +522,7 @@ class SDK:
625
522
  return asyncio.run(
626
523
  self._search_agents_by_reputation_across_chains(
627
524
  agents, tags, reviewers, capabilities, skills, tasks, names,
628
- minAverageScore, includeRevoked, page_size, cursor, sort, chains
525
+ minAverageValue, includeRevoked, page_size, cursor, sort, chains
629
526
  )
630
527
  )
631
528
 
@@ -659,7 +556,7 @@ class SDK:
659
556
  skills=skills,
660
557
  tasks=tasks,
661
558
  names=names,
662
- minAverageScore=minAverageScore,
559
+ minAverageValue=minAverageValue,
663
560
  includeRevoked=includeRevoked,
664
561
  first=page_size,
665
562
  skip=skip,
@@ -694,7 +591,7 @@ class SDK:
694
591
  mcpResources=reg_file.get('mcpResources', []),
695
592
  active=reg_file.get('active', True),
696
593
  x402support=reg_file.get('x402support', False),
697
- extras={'averageScore': agent_data.get('averageScore')}
594
+ extras={'averageValue': agent_data.get('averageValue')}
698
595
  )
699
596
  results.append(agent_summary)
700
597
 
@@ -713,7 +610,7 @@ class SDK:
713
610
  skills: Optional[List[str]],
714
611
  tasks: Optional[List[str]],
715
612
  names: Optional[List[str]],
716
- minAverageScore: Optional[int],
613
+ minAverageValue: Optional[float],
717
614
  includeRevoked: bool,
718
615
  page_size: int,
719
616
  cursor: Optional[str],
@@ -771,7 +668,7 @@ class SDK:
771
668
  skills=skills,
772
669
  tasks=tasks,
773
670
  names=names,
774
- minAverageScore=minAverageScore,
671
+ minAverageValue=minAverageValue,
775
672
  includeRevoked=includeRevoked,
776
673
  first=page_size * 3, # Fetch extra to allow for filtering/sorting
777
674
  skip=0, # We'll handle pagination after aggregation
@@ -851,14 +748,14 @@ class SDK:
851
748
  mcpResources=reg_file.get('mcpResources', []),
852
749
  active=reg_file.get('active', True),
853
750
  x402support=reg_file.get('x402support', False),
854
- extras={'averageScore': agent_data.get('averageScore')}
751
+ extras={'averageValue': agent_data.get('averageValue')}
855
752
  )
856
753
  results.append(agent_summary)
857
754
 
858
- # Sort by averageScore (descending) if available, otherwise by createdAt
755
+ # Sort by averageValue (descending) if available, otherwise by createdAt
859
756
  results.sort(
860
757
  key=lambda x: (
861
- x.extras.get('averageScore') if x.extras.get('averageScore') is not None else 0,
758
+ x.extras.get('averageValue') if x.extras.get('averageValue') is not None else 0,
862
759
  x.chainId,
863
760
  x.agentId
864
761
  ),
@@ -884,46 +781,35 @@ class SDK:
884
781
  }
885
782
 
886
783
  # Feedback methods - delegate to feedback_manager
887
- def signFeedbackAuth(
888
- self,
889
- agentId: "AgentId",
890
- clientAddress: "Address",
891
- indexLimit: Optional[int] = None,
892
- expiryHours: int = 24,
893
- ) -> bytes:
894
- """Sign feedback authorization for a client."""
895
- return self.feedback_manager.signFeedbackAuth(
896
- agentId, clientAddress, indexLimit, expiryHours
897
- )
898
-
899
- def prepareFeedback(
900
- self,
901
- agentId: "AgentId",
902
- score: Optional[int] = None, # 0-100
903
- tags: List[str] = None,
904
- text: Optional[str] = None,
905
- capability: Optional[str] = None,
906
- name: Optional[str] = None,
907
- skill: Optional[str] = None,
908
- task: Optional[str] = None,
909
- context: Optional[Dict[str, Any]] = None,
910
- proofOfPayment: Optional[Dict[str, Any]] = None,
911
- extra: Optional[Dict[str, Any]] = None,
912
- ) -> Dict[str, Any]:
913
- """Prepare feedback file (local file/object) according to spec."""
914
- return self.feedback_manager.prepareFeedback(
915
- agentId, score, tags, text, capability, name, skill, task, context, proofOfPayment, extra
916
- )
784
+ def prepareFeedbackFile(self, input: Dict[str, Any]) -> Dict[str, Any]:
785
+ """Prepare an off-chain feedback file payload.
786
+
787
+ This is intentionally off-chain-only; it does not attempt to represent
788
+ the on-chain fields (value/tag1/tag2/endpoint-on-chain).
789
+ """
790
+ return self.feedback_manager.prepareFeedbackFile(input)
917
791
 
918
792
  def giveFeedback(
919
793
  self,
920
794
  agentId: "AgentId",
921
- feedbackFile: Dict[str, Any],
922
- idem: Optional["IdemKey"] = None,
795
+ value: Union[int, float, str],
796
+ tag1: Optional[str] = None,
797
+ tag2: Optional[str] = None,
798
+ endpoint: Optional[str] = None,
799
+ feedbackFile: Optional[Dict[str, Any]] = None,
923
800
  ) -> "Feedback":
924
- """Give feedback (maps 8004 endpoint)."""
801
+ """Give feedback (on-chain first; optional off-chain file upload).
802
+
803
+ - If feedbackFile is None: submit on-chain only (no upload even if IPFS is configured).
804
+ - If feedbackFile is provided: requires IPFS configured; uploads and commits URI/hash on-chain.
805
+ """
925
806
  return self.feedback_manager.giveFeedback(
926
- agentId, feedbackFile, idem
807
+ agentId=agentId,
808
+ value=value,
809
+ tag1=tag1,
810
+ tag2=tag2,
811
+ endpoint=endpoint,
812
+ feedbackFile=feedbackFile,
927
813
  )
928
814
 
929
815
  def getFeedback(
@@ -936,6 +822,37 @@ class SDK:
936
822
  return self.feedback_manager.getFeedback(
937
823
  agentId, clientAddress, feedbackIndex
938
824
  )
825
+
826
+ def searchFeedback(
827
+ self,
828
+ agentId: "AgentId",
829
+ reviewers: Optional[List["Address"]] = None,
830
+ tags: Optional[List[str]] = None,
831
+ capabilities: Optional[List[str]] = None,
832
+ skills: Optional[List[str]] = None,
833
+ tasks: Optional[List[str]] = None,
834
+ names: Optional[List[str]] = None,
835
+ minValue: Optional[float] = None,
836
+ maxValue: Optional[float] = None,
837
+ include_revoked: bool = False,
838
+ first: int = 100,
839
+ skip: int = 0,
840
+ ) -> List["Feedback"]:
841
+ """Search feedback for an agent."""
842
+ return self.feedback_manager.searchFeedback(
843
+ agentId=agentId,
844
+ clientAddresses=reviewers,
845
+ tags=tags,
846
+ capabilities=capabilities,
847
+ skills=skills,
848
+ tasks=tasks,
849
+ names=names,
850
+ minValue=minValue,
851
+ maxValue=maxValue,
852
+ include_revoked=include_revoked,
853
+ first=first,
854
+ skip=skip,
855
+ )
939
856
 
940
857
  def revokeFeedback(
941
858
  self,
@@ -260,7 +260,7 @@ class SubgraphClient:
260
260
  orderDirection: desc
261
261
  ) {{
262
262
  id
263
- score
263
+ value
264
264
  feedbackIndex
265
265
  tag1
266
266
  tag2
@@ -326,8 +326,7 @@ class SubgraphClient:
326
326
  agentId
327
327
  }}
328
328
  totalFeedback
329
- averageScore
330
- scoreDistribution
329
+ averageFeedbackValue
331
330
  totalValidations
332
331
  completedValidations
333
332
  averageValidationScore
@@ -416,7 +415,7 @@ class SubgraphClient:
416
415
  agent { id agentId chainId }
417
416
  clientAddress
418
417
  feedbackIndex
419
- score
418
+ value
420
419
  tag1
421
420
  tag2
422
421
  endpoint
@@ -516,11 +515,11 @@ class SubgraphClient:
516
515
  # Join all tag alternatives (each already contains complete filter set)
517
516
  tag_filter_condition = ", ".join([f"{{ {item} }}" for item in tag_where_items])
518
517
 
519
- if params.minScore is not None:
520
- where_conditions.append(f'score_gte: {params.minScore}')
518
+ if params.minValue is not None:
519
+ where_conditions.append(f'value_gte: "{params.minValue}"')
521
520
 
522
- if params.maxScore is not None:
523
- where_conditions.append(f'score_lte: {params.maxScore}')
521
+ if params.maxValue is not None:
522
+ where_conditions.append(f'value_lte: "{params.maxValue}"')
524
523
 
525
524
  # Feedback file filters
526
525
  feedback_file_filters = []
@@ -566,7 +565,7 @@ class SubgraphClient:
566
565
  agent {{ id agentId chainId }}
567
566
  clientAddress
568
567
  feedbackIndex
569
- score
568
+ value
570
569
  tag1
571
570
  tag2
572
571
  endpoint
@@ -616,7 +615,7 @@ class SubgraphClient:
616
615
  skills: Optional[List[str]] = None,
617
616
  tasks: Optional[List[str]] = None,
618
617
  names: Optional[List[str]] = None,
619
- minAverageScore: Optional[int] = None, # 0-100
618
+ minAverageValue: Optional[float] = None,
620
619
  includeRevoked: bool = False,
621
620
  first: int = 100,
622
621
  skip: int = 0,
@@ -633,7 +632,7 @@ class SubgraphClient:
633
632
  capabilities: List of capabilities to filter feedback by
634
633
  skills: List of skills to filter feedback by
635
634
  tasks: List of tasks to filter feedback by
636
- minAverageScore: Minimum average score (0-100) for included agents
635
+ minAverageValue: Minimum average value for included agents
637
636
  includeRevoked: Whether to include revoked feedback in calculations
638
637
  first: Number of results to return
639
638
  skip: Number of results to skip
@@ -641,7 +640,7 @@ class SubgraphClient:
641
640
  order_direction: Sort direction (asc/desc)
642
641
 
643
642
  Returns:
644
- List of agents with averageScore field calculated from filtered feedback
643
+ List of agents with averageValue field calculated from filtered feedback
645
644
  """
646
645
  # Build feedback filter
647
646
  feedback_filters = []
@@ -790,7 +789,7 @@ class SubgraphClient:
790
789
  createdAt
791
790
  }}
792
791
  feedback(where: {feedback_where_for_agents}) {{
793
- score
792
+ value
794
793
  isRevoked
795
794
  feedbackFile {{
796
795
  capability
@@ -813,37 +812,33 @@ class SubgraphClient:
813
812
 
814
813
  agents_result = result.get('agents', [])
815
814
 
816
- # Calculate average scores
815
+ # Calculate average values
817
816
  for agent in agents_result:
818
817
  feedbacks = agent.get('feedback', [])
819
818
  if feedbacks:
820
- scores = [int(fb['score']) for fb in feedbacks if fb.get('score', 0) > 0]
821
- if scores:
822
- avg_score = sum(scores) / len(scores)
823
- agent['averageScore'] = avg_score
824
- else:
825
- agent['averageScore'] = None
819
+ values = [float(fb["value"]) for fb in feedbacks if fb.get("value") is not None]
820
+ agent["averageValue"] = (sum(values) / len(values)) if values else None
826
821
  else:
827
- agent['averageScore'] = None
822
+ agent["averageValue"] = None
828
823
 
829
- # Filter by minAverageScore
830
- if minAverageScore is not None:
824
+ # Filter by minAverageValue
825
+ if minAverageValue is not None:
831
826
  agents_result = [
832
827
  agent for agent in agents_result
833
- if agent.get('averageScore') is not None and agent['averageScore'] >= minAverageScore
828
+ if agent.get("averageValue") is not None and agent["averageValue"] >= minAverageValue
834
829
  ]
835
830
 
836
831
  # For reputation search, filter logic:
837
- # - If specific agents were requested, return them even if averageScore is None
832
+ # - If specific agents were requested, return them even if averageValue is None
838
833
  # (the user explicitly asked for these agents, so return them)
839
834
  # - If general search (no specific agents), only return agents with reputation data
840
835
  if agents is None or len(agents) == 0:
841
836
  # General search - only return agents with reputation
842
837
  agents_result = [
843
838
  agent for agent in agents_result
844
- if agent.get('averageScore') is not None
839
+ if agent.get("averageValue") is not None
845
840
  ]
846
- # else: specific agents requested - return all requested agents (even if averageScore is None)
841
+ # else: specific agents requested - return all requested agents (even if averageValue is None)
847
842
 
848
843
  return agents_result
849
844
 
@@ -0,0 +1,91 @@
1
+ """
2
+ Value encoding utilities for ReputationRegistry (Jan 2026).
3
+
4
+ On-chain representation: (value:int256, valueDecimals:uint8)
5
+ Human representation: value / 10^valueDecimals
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from decimal import Decimal, ROUND_HALF_UP, getcontext
12
+ from typing import Tuple, Union
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Plenty of headroom for scaling and clamping checks
17
+ getcontext().prec = 120
18
+
19
+ MAX_DECIMALS = 18
20
+ # Solidity constant (raw int256 magnitude)
21
+ MAX_ABS_VALUE_RAW = 10**50
22
+
23
+
24
+ def encode_feedback_value(input_value: Union[int, float, str, Decimal]) -> Tuple[int, int, str]:
25
+ """
26
+ Encode a user-facing value into the on-chain (value, valueDecimals) pair.
27
+
28
+ Rules:
29
+ - str: parsed using Decimal (no float casting). If >18 decimals, it is rounded half-up to 18 decimals.
30
+ - float: accepted and rounded half-up to 18 decimals (never rejected).
31
+ - int/Decimal: treated similarly; Decimal preserves precision.
32
+
33
+ Returns: (value_raw:int, value_decimals:int, normalized:str)
34
+ """
35
+ if isinstance(input_value, Decimal):
36
+ dec = input_value
37
+ normalized = format(dec, "f")
38
+ elif isinstance(input_value, int):
39
+ dec = Decimal(input_value)
40
+ normalized = str(input_value)
41
+ elif isinstance(input_value, float):
42
+ # Avoid binary float artifacts by going through Decimal(str(x)), then quantize to 18 places.
43
+ dec = Decimal(str(input_value)).quantize(Decimal("1e-18"), rounding=ROUND_HALF_UP)
44
+ normalized = format(dec, "f")
45
+ elif isinstance(input_value, str):
46
+ s = input_value.strip()
47
+ if s == "":
48
+ raise ValueError("value cannot be an empty string")
49
+ dec = Decimal(s)
50
+ # Expand to plain decimal string (no exponent) for determining decimals
51
+ normalized = format(dec, "f")
52
+ else:
53
+ raise TypeError(f"value must be int|float|str|Decimal, got {type(input_value)}")
54
+
55
+ # Determine decimals from the normalized representation.
56
+ # This preserves trailing zeros for string inputs like "1.2300".
57
+ if "." in normalized:
58
+ decimals = len(normalized.split(".", 1)[1])
59
+ else:
60
+ decimals = 0
61
+
62
+ if decimals > MAX_DECIMALS:
63
+ dec = dec.quantize(Decimal("1e-18"), rounding=ROUND_HALF_UP)
64
+ normalized = format(dec, "f") # keeps fixed 18 decimals
65
+ decimals = MAX_DECIMALS
66
+
67
+ scale = Decimal(10) ** decimals
68
+ raw_decimal = dec * scale
69
+ raw_int = int(raw_decimal.to_integral_value(rounding=ROUND_HALF_UP))
70
+
71
+ if abs(raw_int) > MAX_ABS_VALUE_RAW:
72
+ raw_int = MAX_ABS_VALUE_RAW if raw_int > 0 else -MAX_ABS_VALUE_RAW
73
+ clamped = Decimal(raw_int) / (Decimal(10) ** decimals)
74
+ normalized = format(clamped, "f")
75
+ logger.warning(
76
+ "Feedback value %r exceeds on-chain max magnitude; clamped to %s (decimals=%s)",
77
+ input_value,
78
+ normalized,
79
+ decimals,
80
+ )
81
+
82
+ return raw_int, decimals, normalized
83
+
84
+
85
+ def decode_feedback_value(value_raw: int, value_decimals: int) -> float:
86
+ """Decode (value, valueDecimals) into a Python float."""
87
+ if value_decimals < 0:
88
+ raise ValueError("valueDecimals cannot be negative")
89
+ return float(Decimal(value_raw) / (Decimal(10) ** int(value_decimals)))
90
+
91
+