Compare commits

..

64 Commits

Author SHA1 Message Date
nikhilmantri0902
34482d8cc1 refactor(ruletypes): type the rule view data column with a storage-owned struct 2026-09-28 11:18:31 +05:30
nikhilmantri0902
a541d2687c Merge branch 'main' into feat/alert_rule_views
# Conflicts:
#	docs/api/openapi.yml
#	tests/integration/tests/ruler/07_rule_views.py
2026-09-28 10:45:51 +05:30
nikhilmantri0902
6ab70762a7 fix(ruler): use a snake_case slog key for rule view decode errors 2026-09-24 13:42:12 +05:30
nikhilmantri0902
8989e0a3a1 chore(api): regenerate openapi spec and frontend client 2026-09-24 13:34:00 +05:30
nikhilmantri0902
0d150554c7 refactor(ruletypes): split rule view storage and wire shapes
StorableRuleView and GettableRuleView meet only in converters; the list
conversion is a pure types function with per-id errors the provider logs.
Also renames test cases to the PascalCase convention, drops the dead
ensure_notification_channel helper and restores the create_rule_view
fixture lost in the main merge.
2026-09-24 13:34:00 +05:30
nikhilmantri0902
bee75c8e34 Merge branch 'main' into feat/alert_rule_views
# Conflicts:
#	docs/api/openapi.yml
#	ee/sqlstore/postgressqlstore/formatter_test.go
#	frontend/src/api/generated/services/rules/index.ts
#	frontend/src/api/generated/services/sigNoz.schemas.ts
#	pkg/query-service/rules/manager.go
#	pkg/query-service/rules/manager_test.go
#	pkg/ruler/rulestore/rulestoretest/rule.go
#	pkg/ruler/rulestore/sqlrulestore/rule.go
#	pkg/signoz/provider.go
#	pkg/sqlstore/sqlitesqlstore/formatter_test.go
#	pkg/sqlstore/sqlstoretest/formatter_test.go
#	pkg/types/ruletypes/list.go
#	pkg/types/ruletypes/list_test.go
#	pkg/types/ruletypes/listable_rule_test.go
#	pkg/types/ruletypes/rule.go
#	tests/fixtures/alerts.py
2026-09-24 13:09:42 +05:30
Nikhil Mantri
eb54542ca1 Merge branch 'feat/alerts_listing_page_revamp' into feat/alert_rule_views 2026-09-22 12:17:03 +05:30
nikhilmantri0902
c92fe4969a fix(ruletypes): rank nodata above pending in the state display order 2026-09-22 12:00:13 +05:30
nikhilmantri0902
97a2fefe61 Merge branch 'feat/alerts_listing_page_revamp' into feat/alert_rule_views
# Conflicts:
#	pkg/signoz/provider.go
#	pkg/types/ruletypes/list.go
#	pkg/types/ruletypes/list_test.go
2026-09-22 11:32:54 +05:30
Nikhil Mantri
c059d3b8f6 Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-22 11:16:36 +05:30
nikhilmantri0902
37d342eb90 refactor(ruletypes): move the storable-to-listable loop into the types package 2026-09-22 11:16:08 +05:30
nikhilmantri0902
d60da7b8a7 docs(sqlrulestore): fix stale resolver comment on unknown keys 2026-09-22 11:03:52 +05:30
nikhilmantri0902
8d5111bc7d refactor(ruletypes): rename storable-to-listable converter to ToListableRule method 2026-09-22 10:59:50 +05:30
nikhilmantri0902
e2578c15fb revert(rules): drop the drive-by TriggeredAlerts read-lock fix 2026-09-21 16:59:07 +05:30
Nikhil Mantri
a446a832ae Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-21 16:48:30 +05:30
nikhilmantri0902
c89c19ce9e chore(api): regenerate openapi spec and frontend client 2026-09-21 16:43:44 +05:30
nikhilmantri0902
e543b3ef32 feat(sqlrulestore): match bare label keys and both sides of reserved-key collisions
A non-reserved key filters the rule labels directly. A key colliding with a
reserved keyword matches either interpretation, with negative operators
excluding both, mirroring the v5 querier's ambiguous-key semantics.
labels.<key> stays the explicit label-only form.
2026-09-21 16:43:43 +05:30
nikhilmantri0902
3f7531f444 refactor(ruletypes): co-locate state display rank with severity and pin exhaustiveness 2026-09-21 14:30:32 +05:30
nikhilmantri0902
abfa9bee1e fix(rules): validate list params in the manager for non-API callers 2026-09-21 14:13:27 +05:30
nikhilmantri0902
2f497108df fix(ruletypes): lower rules list max page size to 200 2026-09-21 13:54:44 +05:30
nikhilmantri0902
1fa1e292e9 test(sqlstore): cover JSONExtractMapValue in sqlite, postgres and test formatters 2026-09-21 13:47:25 +05:30
nikhilmantri0902
a328988a15 chore: alertStates -> GetAlertStates 2026-09-21 13:28:45 +05:30
nikhilmantri0902
7e53bd607d chore: added ruletypes layer in between structs 2026-09-21 13:26:23 +05:30
Nikhil Mantri
4d82eb8436 Merge branch 'feat/alerts_listing_page_revamp' into feat/alert_rule_views 2026-09-17 12:34:36 +05:30
Nikhil Mantri
95c11d0550 Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-17 12:34:23 +05:30
Nikhil Mantri
b2810ec117 Merge branch 'feat/alerts_listing_page_revamp' into feat/alert_rule_views 2026-09-16 15:48:59 +05:30
Naman Verma
7556dd2868 Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-16 11:28:20 +05:30
Nikhil Mantri
339a10217e Merge branch 'feat/alerts_listing_page_revamp' into feat/alert_rule_views 2026-09-10 18:57:09 +05:30
Nikhil Mantri
cd0eb62734 Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-10 18:56:27 +05:30
nikhilmantri0902
a667007991 Merge remote-tracking branch 'origin/feat/alerts_listing_page_revamp' into feat/alert_rule_views 2026-09-10 16:04:32 +05:30
nikhilmantri0902
208fc1e8ec Merge remote-tracking branch 'origin/feat/common_out_visitors_and_sql_parser' into feat/alerts_listing_page_revamp 2026-09-10 15:58:38 +05:30
nikhilmantri0902
54eb67392a refactor(filterquery): order the compiler main flow first
Visit methods follow Compile; builders, extractors and operator
spelling sit below with main's section separators.
2026-09-10 15:53:07 +05:30
nikhilmantri0902
0e0d97c16f refactor(filterquery): address review nits on the compiler
Verb-first helper names (BuildStringOperation etc., ResolveFreeText),
Sb and Formatter as exported fields instead of getters, and tests for
the dangling-backslash rejection.
2026-09-10 15:43:58 +05:30
nikhilmantri0902
659f9c7ecf test(ruletypes): assert UpdatedAt strictly advances on view update 2026-09-10 13:09:54 +05:30
nikhilmantri0902
ce8442c17a chore(ruler): drop rationale comments from rule view code 2026-09-10 13:09:53 +05:30
nikhilmantri0902
7ab21678b5 test(alerts): own rule view cleanup in a fixture
The lifecycle test filters lists by its own view names instead of wiping
the org's views and asserting global counts.
2026-09-10 13:02:47 +05:30
nikhilmantri0902
951bfb66cd fix(ruletypes): never return null states on a rule view
Validate normalizes nil states to an empty slice and the field is marked
non-nullable; regenerates the openapi spec and the frontend api client.
2026-09-10 13:02:42 +05:30
nikhilmantri0902
113685fbdb refactor(sqlmigration): create the rule_view index via bun builder
Matches the package's index precedent and idx_ naming.
2026-09-10 12:45:05 +05:30
nikhilmantri0902
b9c306dd26 refactor(ruletypes): share list filter validation between params and views
ListRulesParams and RuleViewData now embed one ListFilter with a single
Validate, mirroring dashboards; filter errors surface as rule_list_invalid.
2026-09-10 12:36:24 +05:30
nikhilmantri0902
7e71d4507c Merge remote-tracking branch 'origin/feat/common_out_visitors_and_sql_parser' into feat/alerts_listing_page_revamp 2026-09-09 20:10:09 +05:30
nikhilmantri0902
e13cb08097 refactor(filterquery): collapse the builder into the visitor
One Visitor struct now carries sb, fmter and errors like the old
per-feature visitors did; FieldResolver stays the only new concept.
2026-09-09 20:08:33 +05:30
nikhilmantri0902
1e7aaa9f14 feat(ruler): add saved views CRUD for the alerts list page
A saved view stores the v3 list params (query, states, sort, order) in a
new rule_view table and replays them; shapes mirror dashboard views.
2026-09-09 15:57:47 +05:30
nikhilmantri0902
09ab9f4081 Merge branch 'feat/common_out_visitors_and_sql_parser' into feat/alerts_listing_page_revamp 2026-09-09 11:41:05 +05:30
nikhilmantri0902
f945b5f513 refactor(dashboard): port the list filter to the shared sqlcompiler
The visitor moves to a key-policy resolver plus an error-code wrap;
emitted SQL is unchanged, pinned by the existing exact-SQL unit suite.
2026-09-09 11:33:35 +05:30
nikhilmantri0902
8b0ab0ff26 refactor(filterquery): add shared list filter SQL compiler
Extracted from the dashboards list visitor: grammar walk, operator
dispatch, value extraction, LIKE builders and the Compiled output type,
behind a per-feature FieldResolver. Scope is list pages over the
relational store; telemetry queries stay on querybuilder. Also rejects
LIKE and ILIKE patterns ending in an unescaped backslash, which never
match on sqlite and abort the query on Postgres.
2026-09-09 11:33:34 +05:30
Nikhil Mantri
f2679a6866 Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-08 17:47:29 +05:30
nikhilmantri0902
253a117849 refactor: address list API nits
Use fmt.Sprintf over string concatenation in the compiler, rule store
and JSON formatters; build the enum value lists once as package vars;
collapse the repeated integration seed blocks into a seed_alert_rules
fixture and drop name-restating fixture docstrings.
2026-09-08 17:42:46 +05:30
nikhilmantri0902
c9538be38f style: trim list API comments to one line per the repo comment rules 2026-09-08 17:16:18 +05:30
nikhilmantri0902
be51c37317 fix(sqlcompiler): reject LIKE patterns ending in an unescaped backslash
Such a pattern never matches on sqlite and aborts the query on Postgres
when the matcher consumes the dangling escape, turning user input into
a data-dependent 500. Reject it as a 400 on both list endpoints; a
literal trailing backslash stays expressible as an escaped backslash.
Also move the Compiled type into sqlcompiler, leaving each feature only
the error-code wrap.
2026-09-08 17:16:11 +05:30
nikhilmantri0902
66796d1767 refactor: extract shared filter query SQL compiler from dashboards and rules
The dashboards and rules list visitors were near-identical: boolean
composition, operator dispatch, value extraction and the LIKE family
builders. Move that core to pkg/parser/filterquery/sqlcompiler behind a
FieldResolver interface; each feature keeps only its key policy. Scope
is the bun-managed relational store, telemetry stays on querybuilder.
Verified by both features' unchanged exact-SQL unit suites, the
dashboard integration suite and the rules list suite on both providers.
2026-09-08 15:44:38 +05:30
nikhilmantri0902
8e19b17855 test(sqlrulestore): pin deep-nesting compile cases before visitor extraction
Four hand-derived cases covering mixed predicate kinds, parenthesized
OR groups, NOT over a group and three-level nesting with free text and
a timestamp, matching the depth of dashboards' ComplexExamples suite.
2026-09-08 15:06:11 +05:30
nikhilmantri0902
8885497e14 chore: further cleanup 2026-09-08 12:31:33 +05:30
nikhilmantri0902
626e047485 refactor: trim comments to constraint-only per repo comment rules
Rationale and restatements move out of source; dialect reasoning for
LIKE ESCAPE and ILIKE lowering is documented once in the PR body.
2026-09-08 12:13:16 +05:30
nikhilmantri0902
cc3d86c3ec fix(rules): take the read lock in TriggeredAlerts
The lock has been commented out since the method landed, leaving the
rules map read unguarded against concurrent manager writes. Same class
of race this branch fixed in ListRuleStates and GetRule.
2026-09-08 11:54:05 +05:30
nikhilmantri0902
0100083284 fix(sqlstore): drop unusable quote escape from sqlite JSON map paths
sqlite JSON paths have no backslash escapes, so escaping a double quote
in the key produced a path sqlite cannot parse. The character is also
unreachable: the filter grammar's KEY token cannot contain a quote.
Keep the backslash escape, which matches sqlite's raw key comparison.
2026-09-08 11:44:05 +05:30
Nikhil Mantri
648e945471 Merge branch 'main' into feat/alerts_listing_page_revamp 2026-09-08 11:07:26 +05:30
nikhilmantri0902
7323d5ae0b fix(ruletypes): make list sort deterministic on ties
Ties on the primary sort key kept arbitrary DB order, so rows could
shuffle between page requests causing overlaps or misses. Break ties on
name (case-insensitive) then id, always ascending, with the requested
order applied to the primary key only. Pin the tiebreak in unit tests
and paginate over state ties in the integration suite.
2026-09-08 01:16:39 +05:30
nikhilmantri0902
578b9172ba test(integration): cover the rules list v3 API on both sqlstore providers
Seeds five rules spanning every filter axis and pins the envelope, slim rows,
DSL filters (incl. dotted label keys and missing-label-as-empty semantics),
states param, sort ranks, pagination totals and the error contract; adds
delete_all_rules and an idempotent ensure_notification_channel fixture fn.
Verified against both sqlite and postgres providers.
2026-09-07 20:20:54 +05:30
nikhilmantri0902
2110006d17 feat(sqlrulestore): treat a missing label as empty string for all label value operators
COALESCE applies uniformly instead of only on negations, so labels.key = ''
also matches label-less rules; EXISTS/NOT EXISTS remain the presence checks.
2026-09-07 17:39:41 +05:30
nikhilmantri0902
01dda19d17 feat(sqlrulestore): align label filter semantics with the querier
severity takes the labels operator set (it is an alias for labels.severity,
so EXISTS/NOT EXISTS now work on it), and negative label operators evaluate a
missing label as the empty string instead of always matching, mirroring the
querier's AddDefaultExistsFilter map-attribute semantics.
2026-09-07 17:27:07 +05:30
nikhilmantri0902
b519574b89 feat(ruler): add GET /api/v3/rules route and regenerate API clients
Registers ListRulesV3 with the list params and envelope, marks the v2
ListRules operation deprecated, and regenerates the OpenAPI spec and the
frontend client.
2026-09-07 14:54:46 +05:30
nikhilmantri0902
629ecabce7 feat(ruler): list rules with SQL filter pushdown, state overlay and in-memory pagination
DSL query compiles into the store's WHERE; state is overlaid from a snapshot
of the rule manager map taken under RLock, then the states filter, total,
sort (state/severity rank comparators) and offset/limit run in code so total
always matches what is pageable. StorableRule gains alias:rule to match the
compiler's column references. Also guards the previously unlocked m.rules
reads in ListRuleStates and GetRule.
2026-09-07 14:43:40 +05:30
nikhilmantri0902
131e302c55 feat(sqlrulestore): compile rule list filter DSL to SQL
Adds JSONExtractMapValue to SQLFormatter so a labels map key is one path
segment (dotted label keys work on both dialects), and a visitor over the
shared filterquery grammar mapping rule list DSL keys to SQL.
2026-09-07 13:58:30 +05:30
nikhilmantri0902
239c92ba67 feat(ruletypes): add list params, filter allow-lists and listable rule types for rules list API 2026-09-07 12:48:49 +05:30
132 changed files with 2770 additions and 5129 deletions

View File

@@ -68,7 +68,6 @@ jobs:
- semconvfamilies
- serviceaccount
- spanmapper
- tracedetail
- querier_json_body
- querier_skip_resource_fingerprint
- ttl

View File

@@ -1,44 +1,5 @@
components:
schemas:
AiobservabilitytypesMessage:
properties:
content:
items:
$ref: '#/components/schemas/AiobservabilitytypesPart'
type: array
finishReason:
type: string
role:
type: string
required:
- content
type: object
AiobservabilitytypesPart:
properties:
arguments: {}
content:
type: string
id:
type: string
isError:
type: boolean
name:
type: string
toolCallId:
type: string
type:
$ref: '#/components/schemas/AiobservabilitytypesPartType'
required:
- type
type: object
AiobservabilitytypesPartType:
enum:
- text
- thinking
- tool_call
- tool_result
- generic
type: string
AlertmanagertypesChannel:
properties:
createdAt:
@@ -8967,6 +8928,30 @@ components:
- kind
- spec
type: object
RuletypesGettableRuleView:
properties:
createdAt:
format: date-time
type: string
data:
$ref: '#/components/schemas/RuletypesRuleViewData'
id:
type: string
name:
type: string
orgId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- data
- orgId
- createdAt
- updatedAt
type: object
RuletypesGettableTestRule:
properties:
alertCount:
@@ -9034,6 +9019,15 @@ components:
- alertType
- ruleType
type: object
RuletypesListableRuleViews:
properties:
views:
items:
$ref: '#/components/schemas/RuletypesGettableRuleView'
type: array
required:
- views
type: object
RuletypesListableRules:
properties:
labels:
@@ -9131,6 +9125,16 @@ components:
- ruleType
- condition
type: object
RuletypesPostableRuleView:
properties:
data:
$ref: '#/components/schemas/RuletypesRuleViewData'
name:
type: string
required:
- name
- data
type: object
RuletypesQueryType:
enum:
- builder
@@ -9269,6 +9273,23 @@ components:
- promql_rule
- anomaly_rule
type: string
RuletypesRuleViewData:
properties:
order:
$ref: '#/components/schemas/RuletypesListOrder'
query:
type: string
sort:
$ref: '#/components/schemas/RuletypesListSort'
states:
items:
type: string
type: array
version:
type: string
required:
- version
type: object
RuletypesScheduleType:
enum:
- hourly
@@ -9686,17 +9707,6 @@ components:
required:
- aggregations
type: object
SpantypesGettableTraceThread:
properties:
nextCursor:
type: string
spans:
items:
$ref: '#/components/schemas/SpantypesThreadSpan'
type: array
required:
- spans
type: object
SpantypesGettableWaterfallTrace:
properties:
endTimestampMillis:
@@ -10011,92 +10021,6 @@ components:
nullable: true
type: object
type: object
SpantypesThreadSpan:
properties:
attributes:
additionalProperties: {}
nullable: true
type: object
db_name:
type: string
db_operation:
type: string
duration_nano:
minimum: 0
type: integer
events:
items:
$ref: '#/components/schemas/SpantypesEvent'
nullable: true
type: array
external_http_method:
type: string
external_http_url:
type: string
flags:
minimum: 0
type: integer
formatted_input:
items:
$ref: '#/components/schemas/AiobservabilitytypesMessage'
type: array
formatted_output:
items:
$ref: '#/components/schemas/AiobservabilitytypesMessage'
type: array
has_children:
type: boolean
has_error:
type: boolean
http_host:
type: string
http_method:
type: string
http_url:
type: string
is_remote:
type: string
kind_string:
type: string
level:
minimum: 0
type: integer
name:
type: string
parent_span_id:
type: string
references:
items:
$ref: '#/components/schemas/SpantypesOtelSpanRef'
type: array
resource:
additionalProperties:
type: string
nullable: true
type: object
response_status_code:
type: string
span_id:
type: string
status_code:
type: integer
status_code_string:
type: string
status_message:
type: string
sub_tree_node_count:
minimum: 0
type: integer
time_unix:
minimum: 0
type: integer
trace_id:
type: string
trace_state:
type: string
required:
- references
type: object
SpantypesUpdatableSpanMapper:
properties:
config:
@@ -15821,81 +15745,6 @@ paths:
tags:
- tracedetail
x-signoz-stability: alpha
/api/v1/traces/{traceID}/thread:
get:
deprecated: false
description: Returns the spans carrying gen_ai input or output messages in timestamp
order, each with the messages normalised into formatted_input and formatted_output.
Pages are fetched with the returned nextCursor.
operationId: GetTraceThread
parameters:
- in: query
name: limit
schema:
type: integer
- in: query
name: cursor
schema:
type: string
- in: path
name: traceID
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableTraceThread'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get thread view for a trace
tags:
- tracedetail
x-signoz-stability: alpha
/api/v1/user/me:
get:
deprecated: true
@@ -21358,6 +21207,235 @@ paths:
tags:
- users
x-signoz-stability: alpha
/api/v2/rule_views:
get:
deprecated: false
description: Returns every saved view in the calling user's org. Saved views
are shared org-wide.
operationId: ListRuleViews
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesListableRuleViews'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List rule saved views
tags:
- rules
x-signoz-stability: alpha
post:
deprecated: false
description: Persists the calling user's rule listing state (query, states,
sort, order) as a named, reusable view shared across the org.
operationId: CreateRuleView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostableRuleView'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesGettableRuleView'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Create rule saved view
tags:
- rules
x-signoz-stability: alpha
/api/v2/rule_views/{id}:
delete:
deprecated: false
description: Removes a saved view. Saved views are shared org-wide. Deleting
a non-existent view returns 404.
operationId: DeleteRuleView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Delete rule saved view
tags:
- rules
x-signoz-stability: alpha
put:
deprecated: false
description: Replaces a saved view's name and data. Saved views are shared org-wide.
operationId: UpdateRuleView
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RuletypesPostableRuleView'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RuletypesGettableRuleView'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Update rule saved view
tags:
- rules
x-signoz-stability: alpha
/api/v2/rules:
get:
deprecated: true

View File

@@ -19,7 +19,9 @@ import type {
import type {
CreateRule201,
CreateRuleView201,
DeleteRuleByIDPathParameters,
DeleteRuleViewPathParameters,
GetRuleByID200,
GetRuleByIDPathParameters,
GetRuleHistoryFilterKeys200,
@@ -40,6 +42,7 @@ import type {
GetRuleHistoryTopContributors200,
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
ListRuleViews200,
ListRules200,
ListRulesV3200,
ListRulesV3Params,
@@ -47,8 +50,11 @@ import type {
PatchRuleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPostableRuleDTO,
RuletypesPostableRuleViewDTO,
TestRule200,
UpdateRuleByIDPathParameters,
UpdateRuleView200,
UpdateRuleViewPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -74,6 +80,351 @@ const withQueryKey = <T extends object, K>(
return result;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List rule saved views
*/
export const listRuleViews = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListRuleViews200>({
url: `/api/v2/rule_views`,
method: 'GET',
signal,
});
};
export const getListRuleViewsQueryKey = () => {
return [`/api/v2/rule_views`] as const;
};
export const getListRuleViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listRuleViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listRuleViews>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListRuleViewsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRuleViews>>> = ({
signal,
}) => listRuleViews(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listRuleViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListRuleViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listRuleViews>>
>;
export type ListRuleViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List rule saved views
*/
export function useListRuleViews<
TData = Awaited<ReturnType<typeof listRuleViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listRuleViews>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListRuleViewsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary List rule saved views
*/
export const invalidateListRuleViews = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListRuleViewsQueryKey() },
options,
);
return queryClient;
};
/**
* Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.
* @summary Create rule saved view
*/
export const createRuleView = (
ruletypesPostableRuleViewDTO?: BodyType<RuletypesPostableRuleViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRuleView201>({
url: `/api/v2/rule_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleViewDTO,
signal,
});
};
export const getCreateRuleViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
> => {
const mutationKey = ['createRuleView'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRuleView>>,
{ data?: BodyType<RuletypesPostableRuleViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createRuleView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateRuleViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createRuleView>>
>;
export type CreateRuleViewMutationBody =
| BodyType<RuletypesPostableRuleViewDTO>
| undefined;
export type CreateRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create rule saved view
*/
export const useCreateRuleView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRuleView>>,
TError,
{ data?: BodyType<RuletypesPostableRuleViewDTO> },
TContext
> => {
return useMutation(getCreateRuleViewMutationOptions(options));
};
/**
* Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.
* @summary Delete rule saved view
*/
export const deleteRuleView = (
{ id }: DeleteRuleViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/rule_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteRuleViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
> => {
const mutationKey = ['deleteRuleView'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRuleView>>,
{ pathParams: DeleteRuleViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRuleView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRuleViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRuleView>>
>;
export type DeleteRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete rule saved view
*/
export const useDeleteRuleView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRuleView>>,
TError,
{ pathParams: DeleteRuleViewPathParameters },
TContext
> => {
return useMutation(getDeleteRuleViewMutationOptions(options));
};
/**
* Replaces a saved view's name and data. Saved views are shared org-wide.
* @summary Update rule saved view
*/
export const updateRuleView = (
{ id }: UpdateRuleViewPathParameters,
ruletypesPostableRuleViewDTO?: BodyType<RuletypesPostableRuleViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateRuleView200>({
url: `/api/v2/rule_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleViewDTO,
signal,
});
};
export const getUpdateRuleViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
> => {
const mutationKey = ['updateRuleView'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateRuleView>>,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRuleView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRuleViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRuleView>>
>;
export type UpdateRuleViewMutationBody =
| BodyType<RuletypesPostableRuleViewDTO>
| undefined;
export type UpdateRuleViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update rule saved view
*/
export const useUpdateRuleView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRuleView>>,
TError,
{
pathParams: UpdateRuleViewPathParameters;
data?: BodyType<RuletypesPostableRuleViewDTO>;
},
TContext
> => {
return useMutation(getUpdateRuleViewMutationOptions(options));
};
/**
* This endpoint lists all alert rules with their current evaluation state. Deprecated: use ListRulesV3, which supports filtering, sorting and pagination.
* @deprecated

View File

@@ -4,53 +4,6 @@
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
export enum AiobservabilitytypesPartTypeDTO {
text = 'text',
thinking = 'thinking',
tool_call = 'tool_call',
tool_result = 'tool_result',
generic = 'generic',
}
export interface AiobservabilitytypesPartDTO {
arguments?: unknown;
/**
* @type string
*/
content?: string;
/**
* @type string
*/
id?: string;
/**
* @type boolean
*/
isError?: boolean;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
toolCallId?: string;
type: AiobservabilitytypesPartTypeDTO;
}
export interface AiobservabilitytypesMessageDTO {
/**
* @type array
*/
content: AiobservabilitytypesPartDTO[];
/**
* @type string
*/
finishReason?: string;
/**
* @type string
*/
role?: string;
}
export interface AlertmanagertypesChannelDTO {
/**
* @type string
@@ -10224,6 +10177,60 @@ export enum RuletypesEvaluationKindDTO {
rolling = 'rolling',
cumulative = 'cumulative',
}
export enum RuletypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum RuletypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
state = 'state',
severity = 'severity',
}
export interface RuletypesRuleViewDataDTO {
order?: RuletypesListOrderDTO;
/**
* @type string
*/
query?: string;
sort?: RuletypesListSortDTO;
/**
* @type array
*/
states?: string[];
/**
* @type string
*/
version: string;
}
export interface RuletypesGettableRuleViewDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
data: RuletypesRuleViewDataDTO;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
* @format date-time
*/
updatedAt: string;
}
export interface RuletypesGettableTestRuleDTO {
/**
* @type integer
@@ -10246,17 +10253,6 @@ export interface RuletypesLabelPairDTO {
value: string;
}
export enum RuletypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum RuletypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
state = 'state',
severity = 'severity',
}
export type RuletypesListableRuleDTOLabels = { [key: string]: string };
export enum RuletypesRuleTypeDTO {
@@ -10308,6 +10304,13 @@ export interface RuletypesListableRuleDTO {
updatedBy?: string;
}
export interface RuletypesListableRuleViewsDTO {
/**
* @type array
*/
views: RuletypesGettableRuleViewDTO[];
}
export interface RuletypesListableRulesDTO {
/**
* @type array
@@ -10476,6 +10479,14 @@ export interface RuletypesPostableRuleDTO {
version?: string;
}
export interface RuletypesPostableRuleViewDTO {
data: RuletypesRuleViewDataDTO;
/**
* @type string
*/
name: string;
}
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
export type RuletypesRuleDTOLabels = { [key: string]: string };
@@ -11189,165 +11200,6 @@ export interface SpantypesOtelSpanRefDTO {
traceId?: string;
}
export type SpantypesThreadSpanDTOAttributesAnyOf = { [key: string]: unknown };
/**
* @nullable
*/
export type SpantypesThreadSpanDTOAttributes =
SpantypesThreadSpanDTOAttributesAnyOf | null;
export type SpantypesThreadSpanDTOResourceAnyOf = { [key: string]: string };
/**
* @nullable
*/
export type SpantypesThreadSpanDTOResource =
SpantypesThreadSpanDTOResourceAnyOf | null;
export interface SpantypesThreadSpanDTO {
/**
* @type object,null
*/
attributes?: SpantypesThreadSpanDTOAttributes;
/**
* @type string
*/
db_name?: string;
/**
* @type string
*/
db_operation?: string;
/**
* @type integer
* @minimum 0
*/
duration_nano?: number;
/**
* @type array,null
*/
events?: SpantypesEventDTO[] | null;
/**
* @type string
*/
external_http_method?: string;
/**
* @type string
*/
external_http_url?: string;
/**
* @type integer
* @minimum 0
*/
flags?: number;
/**
* @type array
*/
formatted_input?: AiobservabilitytypesMessageDTO[];
/**
* @type array
*/
formatted_output?: AiobservabilitytypesMessageDTO[];
/**
* @type boolean
*/
has_children?: boolean;
/**
* @type boolean
*/
has_error?: boolean;
/**
* @type string
*/
http_host?: string;
/**
* @type string
*/
http_method?: string;
/**
* @type string
*/
http_url?: string;
/**
* @type string
*/
is_remote?: string;
/**
* @type string
*/
kind_string?: string;
/**
* @type integer
* @minimum 0
*/
level?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
parent_span_id?: string;
/**
* @type array
*/
references: SpantypesOtelSpanRefDTO[];
/**
* @type object,null
*/
resource?: SpantypesThreadSpanDTOResource;
/**
* @type string
*/
response_status_code?: string;
/**
* @type string
*/
span_id?: string;
/**
* @type integer
*/
status_code?: number;
/**
* @type string
*/
status_code_string?: string;
/**
* @type string
*/
status_message?: string;
/**
* @type integer
* @minimum 0
*/
sub_tree_node_count?: number;
/**
* @type integer
* @minimum 0
*/
time_unix?: number;
/**
* @type string
*/
trace_id?: string;
/**
* @type string
*/
trace_state?: string;
}
export interface SpantypesGettableTraceThreadDTO {
/**
* @type string
*/
nextCursor?: string;
/**
* @type array
*/
spans: SpantypesThreadSpanDTO[];
}
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
[key: string]: unknown;
};
@@ -13021,30 +12873,6 @@ export type GetTraceAggregations200 = {
status: string;
};
export type GetTraceThreadPathParameters = {
traceID: string;
};
export type GetTraceThreadParams = {
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type string
* @description undefined
*/
cursor?: string;
};
export type GetTraceThread200 = {
data: SpantypesGettableTraceThreadDTO;
/**
* @type string
*/
status: string;
};
export type ListUserPreferences200 = {
/**
* @type array
@@ -13946,6 +13774,36 @@ export type GetUsersByRoleID200 = {
status: string;
};
export type ListRuleViews200 = {
data: RuletypesListableRuleViewsDTO;
/**
* @type string
*/
status: string;
};
export type CreateRuleView201 = {
data: RuletypesGettableRuleViewDTO;
/**
* @type string
*/
status: string;
};
export type DeleteRuleViewPathParameters = {
id: string;
};
export type UpdateRuleViewPathParameters = {
id: string;
};
export type UpdateRuleView200 = {
data: RuletypesGettableRuleViewDTO;
/**
* @type string
*/
status: string;
};
export type ListRules200 = {
/**
* @type array

View File

@@ -4,17 +4,11 @@
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import { useMutation } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
@@ -22,9 +16,6 @@ import type {
GetFlamegraphPathParameters,
GetTraceAggregations200,
GetTraceAggregationsPathParameters,
GetTraceThread200,
GetTraceThreadParams,
GetTraceThreadPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
@@ -36,26 +27,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
const withQueryKey = <T extends object, K>(
query: T,
queryKey: K,
): T & { queryKey: K } => {
const result = { queryKey } as T & { queryKey: K };
for (const key of Object.keys(query)) {
// The explicit queryKey always wins, matching the previous
// `{ ...query, queryKey }` spread where it was set last.
if (key === 'queryKey') {
continue;
}
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => (query as Record<string, unknown>)[key],
});
}
return result;
};
/**
* Computes span aggregations grouped by requested field.
* @summary Get aggregations for a trace
@@ -156,121 +127,6 @@ export const useGetTraceAggregations = <
> => {
return useMutation(getGetTraceAggregationsMutationOptions(options));
};
/**
* Returns the spans carrying gen_ai input or output messages in timestamp order, each with the messages normalised into formatted_input and formatted_output. Pages are fetched with the returned nextCursor.
* @summary Get thread view for a trace
*/
export const getTraceThread = (
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetTraceThread200>({
url: `/api/v1/traces/${traceID}/thread`,
method: 'GET',
params,
signal,
});
};
export const getGetTraceThreadQueryKey = (
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
) => {
return [
`/api/v1/traces/${traceID}/thread`,
...(params ? [params] : []),
] as const;
};
export const getGetTraceThreadQueryOptions = <
TData = Awaited<ReturnType<typeof getTraceThread>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getTraceThread>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetTraceThreadQueryKey({ traceID }, params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getTraceThread>>> = ({
signal,
}) => getTraceThread({ traceID }, params, signal);
return {
queryKey,
queryFn,
enabled: traceID !== null && traceID !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getTraceThread>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetTraceThreadQueryResult = NonNullable<
Awaited<ReturnType<typeof getTraceThread>>
>;
export type GetTraceThreadQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get thread view for a trace
*/
export function useGetTraceThread<
TData = Awaited<ReturnType<typeof getTraceThread>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getTraceThread>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetTraceThreadQueryOptions(
{ traceID },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get thread view for a trace
*/
export const invalidateGetTraceThread = async (
queryClient: QueryClient,
{ traceID }: GetTraceThreadPathParameters,
params?: GetTraceThreadParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetTraceThreadQueryKey({ traceID }, params) },
options,
);
return queryClient;
};
/**
* Returns the flamegraph view of spans for a given trace ID.
* @summary Get flamegraph view for a trace

View File

@@ -132,6 +132,64 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListRuleViews), handler.OpenAPIDef{
ID: "ListRuleViews",
Tags: []string{"rules"},
Summary: "List rule saved views",
Description: "Returns every saved view in the calling user's org. Saved views are shared org-wide.",
Response: new(ruletypes.ListableRuleViews),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.CreateRuleView), handler.OpenAPIDef{
ID: "CreateRuleView",
Tags: []string{"rules"},
Summary: "Create rule saved view",
Description: "Persists the calling user's rule listing state (query, states, sort, order) as a named, reusable view shared across the org.",
Request: new(ruletypes.PostableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.GettableRuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.UpdateRuleView), handler.OpenAPIDef{
ID: "UpdateRuleView",
Tags: []string{"rules"},
Summary: "Update rule saved view",
Description: "Replaces a saved view's name and data. Saved views are shared org-wide.",
Request: new(ruletypes.UpdatableRuleView),
RequestContentType: "application/json",
Response: new(ruletypes.GettableRuleView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/rule_views/{id}", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.DeleteRuleView), handler.OpenAPIDef{
ID: "DeleteRuleView",
Tags: []string{"rules"},
Summary: "Delete rule saved view",
Description: "Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.",
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusNoContent,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/downtime_schedules", handler.New(provider.authzMiddleware.ViewAccess(provider.rulerHandler.ListDowntimeSchedules), handler.OpenAPIDef{
ID: "ListDowntimeSchedules",
Tags: []string{"downtimeschedules"},

View File

@@ -67,23 +67,5 @@ func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
return err
}
if err := router.Handle("/api/v1/traces/{traceID}/thread", handler.New(
provider.authzMiddleware.ViewAccess(provider.traceDetailHandler.GetThread),
handler.OpenAPIDef{
ID: "GetTraceThread",
Tags: []string{"tracedetail"},
Summary: "Get thread view for a trace",
Description: "Returns the spans carrying gen_ai input or output messages in timestamp order, each with the messages normalised into formatted_input and formatted_output. Pages are fetched with the returned nextCursor.",
RequestQuery: new(spantypes.QueryableThread),
Response: new(spantypes.GettableTraceThread),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
},
)).Methods(http.MethodGet).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -75,25 +75,3 @@ func (h *handler) GetFlamegraph(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, result)
}
func (h *handler) GetThread(rw http.ResponseWriter, r *http.Request) {
req := new(spantypes.QueryableThread)
if err := binding.Query.BindQuery(r.URL.Query(), req); err != nil {
render.Error(rw, err)
return
}
query, err := spantypes.NewThreadQuery(req)
if err != nil {
render.Error(rw, err)
return
}
result, err := h.module.GetThread(r.Context(), mux.Vars(r)["traceID"], query)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, result)
}

View File

@@ -173,19 +173,6 @@ func (m *module) getWindowedWaterfall(ctx context.Context, traceID, selectedSpan
), nil
}
func (m *module) GetThread(ctx context.Context, traceID string, query *spantypes.ThreadQuery) (*spantypes.GettableTraceThread, error) {
summary, err := m.store.GetTraceSummary(ctx, traceID)
if err != nil {
return nil, err
}
spans, err := m.store.GetThreadSpans(ctx, traceID, summary, query.Cursor, query.Limit+1)
if err != nil {
return nil, err
}
return spantypes.NewGettableTraceThread(traceID, spans, query.Limit), nil
}
func (m *module) getFullFlamegraph(ctx context.Context, traceID string, summary *spantypes.TraceSummary, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error) {
fullSpans, err := m.store.GetFlamegraphSpans(ctx, traceID, summary.Start, summary.End, nil)
if err != nil {

View File

@@ -11,23 +11,12 @@ import (
"github.com/SigNoz/signoz/pkg/clickhousesql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const colServiceName = `resource_string_service$$$$name` // $ gets escaped so $$$$ converts to $$.
var fullSpanColumns = []string{
"duration_nano", "span_id", "has_error", "kind",
colServiceName, "name",
"attributes_string", "attributes_number", "attributes_bool", "resources_string",
"events", "status_message", "status_code_string", "kind_string", "parent_span_id",
"flags", "is_remote", "trace_state", "status_code",
"db_name", "db_operation", "http_method", "http_url", "http_host",
"external_http_method", "external_http_url", "response_status_code", "links as references",
}
func buildFieldExpr(fieldKey telemetrytypes.TelemetryFieldKey) (string, error) {
switch fieldKey.FieldContext {
case telemetrytypes.FieldContextResource:
@@ -134,8 +123,16 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
return []spantypes.StorableSpan{}, nil
}
sb := sqlbuilder.NewSelectBuilder()
sb.Select("DISTINCT ON (span_id) timestamp")
sb.SelectMore(fullSpanColumns...)
sb.Select(
"DISTINCT ON (span_id) timestamp",
"duration_nano", "span_id", "has_error", "kind",
colServiceName, "name",
"attributes_string", "attributes_number", "attributes_bool", "resources_string",
"events", "status_message", "status_code_string", "kind_string", "parent_span_id",
"flags", "is_remote", "trace_state", "status_code",
"db_name", "db_operation", "http_method", "http_url", "http_host",
"external_http_method", "external_http_url", "response_status_code", "links as references",
)
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
ids := make([]any, len(spanIDs))
for i, id := range spanIDs {
@@ -158,44 +155,6 @@ func (s *traceStore) GetTraceSpansByIDs(ctx context.Context, traceID string, sta
return spans, nil
}
func (s *traceStore) GetThreadSpans(ctx context.Context, traceID string, summary *spantypes.TraceSummary, cursor *spantypes.ThreadCursor, limit int) ([]spantypes.StorableSpan, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("DISTINCT ON (span_id) timestamp")
sb.SelectMore(fullSpanColumns...)
sb.SelectMore("attributes")
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceTable))
sb.Where(
sb.E("trace_id", traceID),
sb.GE("ts_bucket_start", summary.Start.Unix()-1800),
sb.LE("ts_bucket_start", summary.End.Unix()),
// Reads only the JSON column; spans with messages only in the legacy maps are skipped.
// todo(nitya): pick the column from the attribute evolution metadata.
sb.Or(
sqlbuilder.Escape(fmt.Sprintf("attributes.%s IS NOT NULL", clickhousesql.Identifier(aiobservabilitytypes.GenAIInputMessages))),
sqlbuilder.Escape(fmt.Sprintf("attributes.%s IS NOT NULL", clickhousesql.Identifier(aiobservabilitytypes.GenAIOutputMessages))),
),
)
if cursor != nil {
// ClickHouse can't use an index for a tuple comparison, so the separate timestamp and
// ts_bucket_start bounds are what skip the data before the cursor.
sb.Where(
sb.GE("ts_bucket_start", int64(cursor.TimeUnixNano/uint64(time.Second))-1800),
sb.GE("timestamp", fmt.Sprintf("%d", cursor.TimeUnixNano)),
sb.GT("(toUnixTimestamp64Nano(timestamp), span_id)", sqlbuilder.Tuple(cursor.TimeUnixNano, cursor.SpanID)),
)
}
sb.OrderByAsc("timestamp")
sb.OrderByAsc("span_id")
sb.Limit(limit)
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
var spans []spantypes.StorableSpan
if err := s.telemetryStore.ClickhouseDB().Select(ctx, &spans, query, args...); err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying thread spans")
}
return spans, nil
}
func (s *traceStore) GetFlamegraphSpans(ctx context.Context, traceID string, start, end time.Time, spanIDs []string) ([]spantypes.StorableSpan, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(

View File

@@ -13,7 +13,6 @@ type Handler interface {
GetWaterfallV4(http.ResponseWriter, *http.Request)
GetTraceAggregations(http.ResponseWriter, *http.Request)
GetFlamegraph(http.ResponseWriter, *http.Request)
GetThread(http.ResponseWriter, *http.Request)
}
// Module defines the business logic for trace detail operations.
@@ -21,5 +20,4 @@ type Module interface {
GetWaterfallV4(ctx context.Context, traceID string, selectedSpanID string, uncollapsedSpans []string) (*spantypes.GettableWaterfallTrace, error)
GetTraceAggregations(ctx context.Context, traceID string, req *spantypes.PostableTraceAggregations) (*spantypes.GettableTraceAggregations, error)
GetFlamegraph(ctx context.Context, traceID string, selectedSpanID string, selectFields []telemetrytypes.TelemetryFieldKey) (*spantypes.GettableFlamegraphTrace, error)
GetThread(ctx context.Context, traceID string, query *spantypes.ThreadQuery) (*spantypes.GettableTraceThread, error)
}

View File

@@ -566,6 +566,24 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
}, nil
}
// flattenJSONPaths flattens a decoded JSON document into dotted keys, overwriting existing keys in out.
func flattenJSONPaths(prefix string, m map[string]any, out map[string]any) {
for k, v := range m {
key := k
if prefix != "" {
key = prefix + "." + k
}
switch child := v.(type) {
case map[string]any:
flattenJSONPaths(key, child, out)
case telemetrystoretypes.JSONValue:
flattenJSONPaths(key, child, out)
default:
out[key] = v
}
}
}
// mergeSpanAttributeColumns merges (attributes_string, attributes_number, attributes_bool, resources_string) into
// unified "attributes" and "resource" keys, and parses the stringified `events`
// and `links` columns into structured slices. Raw DB columns are removed.
@@ -580,7 +598,7 @@ func mergeSpanAttributeColumns(data map[string]any) {
resStr, hasRes := data["resources_string"]
if hasStr || hasNum || hasBool || attrJSON != nil || hasRes {
attributes := make(map[string]any)
attrJSON.FlattenInto("", attributes)
flattenJSONPaths("", attrJSON, attributes)
if m, ok := attrStr.(map[string]string); ok {
for k, v := range m {
attributes[k] = v

View File

@@ -36,7 +36,7 @@ func TestManager_ListRules_ValidatesParams(t *testing.T) {
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{Limit: -1})
require.ErrorContains(t, err, "invalid limit")
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{States: []string{"bogus"}})
_, err = m.ListRules(context.Background(), &ruletypes.ListRulesParams{ListFilter: ruletypes.ListFilter{States: []string{"bogus"}}})
require.ErrorContains(t, err, `invalid state "bogus"`)
}

View File

@@ -12,6 +12,11 @@ type Handler interface {
PatchRuleByID(http.ResponseWriter, *http.Request)
TestRule(http.ResponseWriter, *http.Request)
ListRuleViews(http.ResponseWriter, *http.Request)
CreateRuleView(http.ResponseWriter, *http.Request)
UpdateRuleView(http.ResponseWriter, *http.Request)
DeleteRuleView(http.ResponseWriter, *http.Request)
ListDowntimeSchedules(http.ResponseWriter, *http.Request)
GetDowntimeScheduleByID(http.ResponseWriter, *http.Request)
CreateDowntimeSchedule(http.ResponseWriter, *http.Request)

View File

@@ -49,4 +49,9 @@ type Ruler interface {
// TODO: expose downtime CRUD as methods on Ruler directly instead of leaking the
// store interface. The handler should not call store methods directly.
MaintenanceStore() alertmanagertypes.MaintenanceStore
CreateRuleView(ctx context.Context, orgID valuer.UUID, postable ruletypes.PostableRuleView) (*ruletypes.GettableRuleView, error)
ListRuleViews(ctx context.Context, orgID valuer.UUID) (*ruletypes.ListableRuleViews, error)
UpdateRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable ruletypes.UpdatableRuleView) (*ruletypes.GettableRuleView, error)
DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
}

View File

@@ -0,0 +1,93 @@
package sqlrulestore
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
func (r *rule) CreateRuleView(ctx context.Context, view *ruletypes.StorableRuleView) error {
_, err := r.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(view).
Exec(ctx)
if err != nil {
return r.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "rule view with id %s already exists", view.ID)
}
return nil
}
func (r *rule) GetRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*ruletypes.StorableRuleView, error) {
view := new(ruletypes.StorableRuleView)
err := r.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(view).
Where("id = ?", id).
Where("org_id = ?", orgID).
Scan(ctx)
if err != nil {
return nil, r.sqlstore.WrapNotFoundErrf(err, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", id)
}
return view, nil
}
func (r *rule) ListRuleViews(ctx context.Context, orgID valuer.UUID) ([]*ruletypes.StorableRuleView, error) {
views := make([]*ruletypes.StorableRuleView, 0)
err := r.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&views).
Where("org_id = ?", orgID).
OrderExpr("updated_at DESC").
Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't list rule views")
}
return views, nil
}
func (r *rule) UpdateRuleView(ctx context.Context, view *ruletypes.StorableRuleView) error {
res, err := r.sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model(view).
WherePK().
Where("org_id = ?", view.OrgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't update rule view")
}
rows, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read rule view update result")
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", view.ID)
}
return nil
}
func (r *rule) DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
res, err := r.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(ruletypes.StorableRuleView)).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't delete rule view")
}
rows, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "couldn't read rule view delete result")
}
if rows == 0 {
return errors.Newf(errors.TypeNotFound, ruletypes.ErrCodeRuleViewNotFound, "rule view with id %s doesn't exist", id)
}
return nil
}

View File

@@ -345,3 +345,122 @@ func (handler *handler) DeleteDowntimeScheduleByID(rw http.ResponseWriter, req *
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) ListRuleViews(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
views, err := handler.ruler.ListRuleViews(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, views)
}
func (handler *handler) CreateRuleView(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
var postable ruletypes.PostableRuleView
if err := binding.JSON.BindBody(req.Body, &postable); err != nil {
render.Error(rw, err)
return
}
view, err := handler.ruler.CreateRuleView(ctx, orgID, postable)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, view)
}
func (handler *handler) UpdateRuleView(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
return
}
var updatable ruletypes.UpdatableRuleView
if err := binding.JSON.BindBody(req.Body, &updatable); err != nil {
render.Error(rw, err)
return
}
view, err := handler.ruler.UpdateRuleView(ctx, orgID, id, updatable)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, view)
}
func (handler *handler) DeleteRuleView(rw http.ResponseWriter, req *http.Request) {
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := valuer.NewUUID(mux.Vars(req)["id"])
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
return
}
if err := handler.ruler.DeleteRuleView(ctx, orgID, id); err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@@ -147,3 +147,41 @@ func (provider *provider) TestNotification(ctx context.Context, orgID valuer.UUI
func (provider *provider) MaintenanceStore() alertmanagertypes.MaintenanceStore {
return provider.manager.MaintenanceStore()
}
func (provider *provider) CreateRuleView(ctx context.Context, orgID valuer.UUID, postable ruletypes.PostableRuleView) (*ruletypes.GettableRuleView, error) {
if err := postable.Validate(); err != nil {
return nil, err
}
storable := postable.ToStorableRuleView(orgID)
if err := provider.ruleStore.CreateRuleView(ctx, storable); err != nil {
return nil, err
}
return storable.ToGettableRuleView(), nil
}
func (provider *provider) ListRuleViews(ctx context.Context, orgID valuer.UUID) (*ruletypes.ListableRuleViews, error) {
storables, err := provider.ruleStore.ListRuleViews(ctx, orgID)
if err != nil {
return nil, err
}
return &ruletypes.ListableRuleViews{Views: ruletypes.NewGettableRuleViewsFromStorableRuleViews(storables)}, nil
}
func (provider *provider) UpdateRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatable ruletypes.UpdatableRuleView) (*ruletypes.GettableRuleView, error) {
if err := updatable.Validate(); err != nil {
return nil, err
}
storable, err := provider.ruleStore.GetRuleView(ctx, orgID, id)
if err != nil {
return nil, err
}
storable.Update(updatable)
if err := provider.ruleStore.UpdateRuleView(ctx, storable); err != nil {
return nil, err
}
return storable.ToGettableRuleView(), nil
}
func (provider *provider) DeleteRuleView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return provider.ruleStore.DeleteRuleView(ctx, orgID, id)
}

View File

@@ -257,6 +257,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddCloudIntegrationTuplesFactory(sqlstore),
sqlmigration.NewAddNotificationChannelTuplesFactory(sqlstore),
sqlmigration.NewAddAIObservabilityQuickFiltersFactory(sqlstore),
sqlmigration.NewAddRuleViewFactory(sqlstore, sqlschema),
)
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
@@ -49,11 +50,6 @@ type rule struct {
OrgID string `bun:"org_id,type:text"`
}
type routePolicyRuleData struct {
PreferredChannels []string `json:"preferredChannels"`
Labels map[string]string `json:"labels"`
}
type addRoutePolicies struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
@@ -191,20 +187,20 @@ func (migration *addRoutePolicies) migrateRulesToRoutePolicies(ctx context.Conte
func (migration *addRoutePolicies) convertRulesToRoutes(rules []*rule, channelsByOrg map[string][]string) ([]*expressionRoute, error) {
var routes []*expressionRoute
for _, r := range rules {
var ruleData routePolicyRuleData
if err := json.Unmarshal([]byte(r.Data), &ruleData); err != nil {
var gettableRule ruletypes.GettableRule
if err := json.Unmarshal([]byte(r.Data), &gettableRule); err != nil {
return nil, errors.NewInternalf(errors.CodeInternal, "failed to unmarshal rule data for rule ID %s: %v", r.ID, err)
}
if len(ruleData.PreferredChannels) == 0 {
if len(gettableRule.PreferredChannels) == 0 {
channels, exists := channelsByOrg[r.OrgID]
if !exists || len(channels) == 0 {
continue
}
ruleData.PreferredChannels = channels
gettableRule.PreferredChannels = channels
}
severity := "critical"
if v, ok := ruleData.Labels["severity"]; ok {
if v, ok := gettableRule.Labels["severity"]; ok {
severity = v
}
expression := fmt.Sprintf(`%s == "%s" && %s == "%s"`, "threshold.name", severity, "ruleId", r.ID.String())
@@ -222,7 +218,7 @@ func (migration *addRoutePolicies) convertRulesToRoutes(rules []*rule, channelsB
},
Expression: expression,
ExpressionKind: "rule",
Channels: ruleData.PreferredChannels,
Channels: gettableRule.PreferredChannels,
Name: r.ID.StringValue(),
Enabled: true,
OrgID: r.OrgID,

View File

@@ -0,0 +1,78 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addRuleView struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddRuleViewFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_rule_view"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &addRuleView{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
})
}
func (migration *addRuleView) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
func (migration *addRuleView) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
sqls := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "rule_view",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
if _, err := tx.NewCreateIndex().
Table("rule_view").
Column("org_id").
Index("idx_rule_view_org_id").
IfNotExists().
Exec(ctx); err != nil {
return err
}
return tx.Commit()
}
func (migration *addRuleView) Down(_ context.Context, _ *bun.DB) error {
return nil
}

View File

@@ -364,26 +364,12 @@ func (provider *provider) gc(ctx context.Context, org *types.Organization) error
}
func (provider *provider) flushLastObservedAt(ctx context.Context, org *types.Organization) error {
tokens, err := provider.tokenStore.ListByOrgID(ctx, org.ID)
accessTokenToLastObservedAt, err := provider.listLastObservedAtDesc(ctx, org.ID)
if err != nil {
return err
}
observedTokens := make([]*authtypes.StorableToken, 0, len(tokens))
for _, token := range tokens {
cachedLastObservedAt, ok := provider.lastObservedAtCache.Get(lastObservedAtCacheKey(token.AccessToken, token.UserID))
if !ok {
continue
}
if err := token.UpdateLastObservedAt(cachedLastObservedAt); err != nil {
continue
}
observedTokens = append(observedTokens, token)
}
if err := provider.tokenStore.UpdateLastObservedAt(ctx, observedTokens); err != nil {
if err := provider.tokenStore.UpdateLastObservedAtByAccessToken(ctx, accessTokenToLastObservedAt); err != nil {
return err
}

View File

@@ -232,16 +232,15 @@ func (store *store) ListByUserID(ctx context.Context, userID valuer.UUID) ([]*au
return tokens, nil
}
func (store *store) UpdateLastObservedAt(ctx context.Context, tokens []*authtypes.StorableToken) error {
if len(tokens) == 0 {
func (store *store) UpdateLastObservedAtByAccessToken(ctx context.Context, accessTokenToLastObservedAt []map[string]any) error {
if len(accessTokenToLastObservedAt) == 0 {
return nil
}
values := store.
sqlstore.
BunDBCtx(ctx).
NewValues(&tokens).
Column("id", "last_observed_at", "updated_at")
NewValues(&accessTokenToLastObservedAt)
_, err := store.
sqlstore.
@@ -251,8 +250,8 @@ func (store *store) UpdateLastObservedAt(ctx context.Context, tokens []*authtype
Model((*authtypes.StorableToken)(nil)).
TableExpr("update_cte").
Set("last_observed_at = update_cte.last_observed_at").
Set("updated_at = update_cte.updated_at").
Where("auth_token.id = update_cte.id").
Where("auth_token.access_token = update_cte.access_token").
Where("auth_token.user_id = update_cte.user_id").
Exec(ctx)
if err != nil {
return err

View File

@@ -1,41 +0,0 @@
package genaimessages
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testdata/captured/<sdk>/<attribute>.txt holds a real attribute value from the scripts
// repo's static-telemetry-generator captures; <attribute>.want.json is its reviewed output.
func TestNormalizeCapturedPayloads(t *testing.T) {
inputs, err := filepath.Glob(filepath.Join("testdata", "captured", "*", "*.txt"))
require.NoError(t, err)
require.NotEmpty(t, inputs)
for _, input := range inputs {
sdk := filepath.Base(filepath.Dir(input))
attribute := strings.TrimSuffix(filepath.Base(input), ".txt")
t.Run(sdk+"/"+attribute, func(t *testing.T) {
raw, err := os.ReadFile(input)
require.NoError(t, err)
wantJSON, err := os.ReadFile(strings.TrimSuffix(input, ".txt") + ".want.json")
require.NoError(t, err)
var want []aiobservabilitytypes.Message
require.NoError(t, json.Unmarshal(wantJSON, &want))
got, err := json.Marshal(Normalize(string(raw)))
require.NoError(t, err)
var gotRoundTrip []aiobservabilitytypes.Message
require.NoError(t, json.Unmarshal(got, &gotRoundTrip))
assert.Equal(t, want, gotRoundTrip)
})
}
}

View File

@@ -1,307 +0,0 @@
package genaimessages
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// convertSemconvMessages handles [{role, parts, finish_reason}] and Gemini contents.
func convertSemconvMessages(value any) ([]aiobservabilitytypes.Message, bool) {
list, ok := value.([]any)
if !ok || len(list) == 0 {
return nil, false
}
first, ok := toObject(firstItem(list))
if !ok {
return nil, false
}
if ok := first.has("parts"); !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(list))
for _, item := range list {
m, ok := toObject(item)
if !ok {
messages = append(messages, genericMessages(stringOf(item))...)
continue
}
messages = append(messages, semconvMessage(m, "")...)
}
return messages, true
}
// convertChatMessageList handles OpenAI, Anthropic, Vercel and LangChain message lists.
func convertChatMessageList(value any) ([]aiobservabilitytypes.Message, bool) {
list, ok := value.([]any)
if !ok || len(list) == 0 {
return nil, false
}
if !isChatMessage(firstItem(list)) {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(list))
for _, item := range list {
messages = append(messages, chatMessage(item, "")...)
}
return messages, true
}
func isChatMessage(value any) bool {
m, ok := toObject(value)
if !ok {
return false
}
if ok := m.has("role"); ok {
return true
}
switch typ := m.str("type"); typ {
case "message", "reasoning", "human", "ai", "tool", "system":
return true
case "constructor":
ok := m.has("kwargs")
return ok
default:
return isResponsesItemType(typ)
}
}
// convertToolCallList handles a bare tool call list, e.g. Vercel ai.response.toolCalls.
func convertToolCallList(value any) ([]aiobservabilitytypes.Message, bool) {
list, ok := value.([]any)
if !ok || len(list) == 0 {
return nil, false
}
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleAssistant, Content: make([]aiobservabilitytypes.Part, 0, len(list))}
for _, item := range list {
call, ok := toObject(item)
if !ok || !isToolCall(call) {
return nil, false
}
msg.Content = append(msg.Content, toolCallPart(call))
}
return []aiobservabilitytypes.Message{msg}, true
}
// isToolCall rejects tool definitions, which carry no arguments.
func isToolCall(call object) bool {
if has := call.has("toolName"); has {
return true
}
if fn, ok := call.obj("function"); ok {
has := fn.has("arguments")
return has
}
if has := call.has("name"); !has {
return false
}
_, has := call.lookup("arguments", "args")
return has
}
// convertChatRequest handles OpenAI, Anthropic, Vercel, Gemini and LangChain request objects.
func convertChatRequest(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
conversation, ok := m.lookup("messages", "input", "contents", "prompt")
if !ok {
return nil, false
}
messages := []aiobservabilitytypes.Message{}
if system := systemMessage(m.first("system", "instructions", "system_instruction", "systemInstruction", "system_prompt")); system != nil {
messages = append(messages, *system)
} else if config, ok := m.obj("config"); ok {
if system := systemMessage(config.first("system_instruction", "systemInstruction")); system != nil {
messages = append(messages, *system)
}
}
// {messages: "[...]"}: the list arrives JSON-encoded once more from some SDKs
if s, isString := conversation.(string); isString {
var decoded any
if err := json.Unmarshal([]byte(s), &decoded); err == nil {
if _, isList := decoded.([]any); isList {
conversation = decoded
}
}
}
switch c := conversation.(type) {
case string:
if c != "" {
messages = append(messages, textMessage(aiobservabilitytypes.MessageRoleUser, c))
}
case []any:
for _, item := range flattenOnce(c) {
if s, isString := item.(string); isString {
messages = append(messages, textMessage(aiobservabilitytypes.MessageRoleUser, s))
continue
}
messages = append(messages, semconvMessage(asObject(item), aiobservabilitytypes.MessageRoleUser)...)
}
case map[string]any:
messages = append(messages, semconvMessage(c, aiobservabilitytypes.MessageRoleUser)...)
default:
return nil, false
}
return messages, true
}
// systemMessage returns nil when value is empty or unknown.
func systemMessage(value any) *aiobservabilitytypes.Message {
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleSystem, Content: []aiobservabilitytypes.Part{}}
switch v := value.(type) {
case string:
if v == "" {
return nil
}
msg.Content = append(msg.Content, textPart(v))
case []any:
for _, item := range v {
msg.Content = append(msg.Content, contentBlockPart(item))
}
case map[string]any:
parts, ok := object(v).list("parts")
if !ok {
return nil
}
for _, p := range parts {
msg.Content = append(msg.Content, semconvPart(p))
}
default:
return nil
}
if len(msg.Content) == 0 {
return nil
}
return &msg
}
// convertChatResponse handles the OpenAI Chat Completions {choices} response.
func convertChatResponse(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
choices, ok := m.list("choices")
if !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(choices))
for _, c := range choices {
choice := asObject(c)
var converted []aiobservabilitytypes.Message
if message, ok := toObject(choice.first("message", "delta")); ok {
converted = chatMessage(message, aiobservabilitytypes.MessageRoleAssistant)
} else if text, ok := choice.get("text"); ok {
converted = []aiobservabilitytypes.Message{textMessage(aiobservabilitytypes.MessageRoleAssistant, stringOf(text))}
} else {
converted = []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{genericPart(choice)}}}
}
messages = append(messages, withFinishReason(converted, choice.str("finish_reason"))...)
}
return messages, true
}
func convertResponsesAPIResponse(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
output, ok := m.list("output")
if !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(output))
for _, item := range output {
messages = append(messages, chatMessage(item, aiobservabilitytypes.MessageRoleAssistant)...)
}
if last := lastMessage(messages); last != nil && last.FinishReason == "" {
if details, ok := m.obj("incomplete_details"); ok {
last.FinishReason = normalizeFinishReason(details.str("reason"))
} else if m.str("status") == "completed" {
last.FinishReason = aiobservabilitytypes.FinishReasonStop
}
}
return messages, true
}
// convertGeminiResponse handles Gemini {candidates} and Google ADK {content} responses.
func convertGeminiResponse(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
if content, ok := m.obj("content"); ok {
if hasParts := content.has("parts"); hasParts {
return withFinishReason(semconvMessage(content, aiobservabilitytypes.MessageRoleAssistant), finishReasonOf(m)), true
}
}
candidates, ok := m.list("candidates")
if !ok {
return nil, false
}
messages := make([]aiobservabilitytypes.Message, 0, len(candidates))
for _, c := range candidates {
candidate := asObject(c)
content, ok := candidate.obj("content")
if !ok {
generic := []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{genericPart(candidate)}}}
messages = append(messages, withFinishReason(generic, finishReasonOf(candidate))...)
continue
}
messages = append(messages, withFinishReason(semconvMessage(content, aiobservabilitytypes.MessageRoleAssistant), finishReasonOf(candidate))...)
}
return messages, true
}
func convertLegacyCompletion(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
completion, ok := m.text("completion")
if !ok {
return nil, false
}
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{}}
if reasoning, ok := m.text("reasoning"); ok && reasoning != "" {
msg.Content = append(msg.Content, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: reasoning})
}
msg.Content = append(msg.Content, textPart(completion))
return []aiobservabilitytypes.Message{msg}, true
}
func convertLangChainGenerations(value any) ([]aiobservabilitytypes.Message, bool) {
m, ok := toObject(value)
if !ok {
return nil, false
}
generations, ok := m.list("generations")
if !ok {
return nil, false
}
messages := []aiobservabilitytypes.Message{}
for _, g := range flattenOnce(generations) {
gen := asObject(g)
var converted []aiobservabilitytypes.Message
if message, ok := gen.obj("message"); ok {
converted = chatMessage(message, aiobservabilitytypes.MessageRoleAssistant)
} else {
converted = []aiobservabilitytypes.Message{textMessage(aiobservabilitytypes.MessageRoleAssistant, gen.str("text"))}
}
messages = append(messages, withFinishReason(converted, asObject(gen.at("generation_info")).str("finish_reason"))...)
}
return messages, true
}
// withFinishReason sets reason on the last message that has none.
func withFinishReason(messages []aiobservabilitytypes.Message, reason string) []aiobservabilitytypes.Message {
if last := lastMessage(messages); last != nil && last.FinishReason == "" {
last.FinishReason = normalizeFinishReason(reason)
}
return messages
}

View File

@@ -1,288 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func TestNormalizeEnvelopeRequests(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "AnthropicRequest_SystemStringBecomesSystemMessage",
raw: `{
"model": "claude",
"system": "Be brief.",
"messages": [{"role": "user", "content": "Hi"}],
"max_tokens": 100
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleSystem,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Be brief."},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
{
name: "GeminiRequest_ConfigSystemInstructionBecomesSystemMessage",
raw: `{
"model": "gemini-2.0",
"config": {"system_instruction": "Be brief."},
"contents": [
{"role": "user", "parts": [{"text": "Hi"}]},
{
"role": "user",
"parts": [
{
"function_response": {"name": "get_weather", "response": {"temp": 20}}
}
]
}
]
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleSystem,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Be brief."},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, Name: "get_weather", Content: `{"temp":20}`},
},
},
},
},
{
name: "GeminiRequest_SystemInstructionPartsBecomeSystemMessage",
raw: `{"system_instruction": {"parts": [{"text": "Be brief."}]}, "contents": [{"role": "user", "parts": [{"text": "Hi"}]}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleSystem,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Be brief."},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
{
name: "RequestWithMessagesAsJSONString_MessagesDecoded",
raw: `{"messages":"[{\"role\":\"user\",\"content\":\"Hi\"}]"}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
})
}
func TestNormalizeEnvelopeChatResponses(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "StreamingChunkDelta_BecomesAssistantText",
raw: `{"choices": [{"delta": {"role": "assistant", "content": "Hel"}, "finish_reason": null}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hel"},
},
},
},
},
{
name: "LegacyCompletionChoiceText_BecomesAssistantTextWithLength",
raw: `{"choices": [{"text": "Hello", "finish_reason": "length"}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonLength,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hello"},
},
},
},
},
{
name: "FilteredChoiceWithoutMessage_BecomesGenericWithContentFilter",
raw: `{"choices": [{"index": 0, "finish_reason": "content_filter"}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonContentFilter,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"finish_reason":"content_filter","index":0}`},
},
},
},
},
})
}
func TestNormalizeEnvelopeOpenAIResponses(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "ResponsesAPIResponse_MessageAndReasoningItemsConverted",
raw: `{
"object": "response",
"status": "completed",
"output": [
{
"type": "reasoning",
"id": "rs_1",
"summary": [{"type": "summary_text", "text": "Thinking about it"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Paris."}]
}
]
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Thinking about it"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Paris."},
},
FinishReason: aiobservabilitytypes.FinishReasonStop,
},
},
},
{
name: "ResponsesAPIResponseIncomplete_EncryptedReasoningEmptyAndFinishReasonLength",
raw: `{
"output": [
{"type": "reasoning", "content": [{"type": "reasoning_text", "text": "step 1"}]},
{"type": "reasoning", "encrypted_content": "gAAAA"}
],
"status": "incomplete",
"incomplete_details": {"reason": "max_output_tokens"}
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "step 1"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonLength,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking},
},
},
},
},
})
}
func TestNormalizeEnvelopeGemini(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "GeminiResponse_ThoughtAndFunctionCallPartsConverted",
raw: `{
"candidates": [
{
"content": {
"parts": [
{"text": "Let me check", "thought": true},
{"function_call": {"name": "get_weather", "args": {"city": "Paris"}}}
],
"role": "model"
},
"finishReason": "STOP"
}
],
"usageMetadata": {}
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonStop,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Let me check"},
{Type: aiobservabilitytypes.PartTypeToolCall, Name: "get_weather", Arguments: map[string]any{"city": "Paris"}},
},
},
},
},
{
name: "GoogleADKResponse_ContentBecomesAssistantMessageWithStop",
raw: `{"content": {"parts": [{"text": "It is 18°C and clear in Bengaluru."}], "role": "model"}, "finish_reason": "STOP"}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonStop,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "It is 18°C and clear in Bengaluru."},
},
},
},
},
{
name: "GeminiResponseBlockedBySafety_BecomesGenericWithContentFilter",
raw: `{"candidates": [{"finishReason": "SAFETY", "index": 0}]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonContentFilter,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"finishReason":"SAFETY","index":0}`},
},
},
},
},
})
}
func TestNormalizeEnvelopeLangChain(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "LangChainGenerationWithOnlyText_BecomesAssistantTextWithLength",
raw: `{"generations": [[{"text": "Observability is the ability", "generation_info": {"finish_reason": "length"}}]]}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonLength,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Observability is the ability"},
},
},
},
},
})
}

View File

@@ -1,71 +0,0 @@
package genaimessages
import (
"encoding/json"
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// Ordered by specificity: earlier converters never match a later format.
var converters = []converter{
convertSemconvMessages,
convertChatMessageList,
convertToolCallList,
convertChatRequest,
convertChatResponse,
convertResponsesAPIResponse,
convertGeminiResponse,
convertLegacyCompletion,
convertLangChainGenerations,
}
// Normalize converts a gen_ai.*.messages value, a JSON string or a decoded value.
// A string that is not JSON becomes one text part; JSON in no known format becomes
// one generic part holding the original.
func Normalize(raw any) []aiobservabilitytypes.Message {
var (
value any
original string
)
switch v := raw.(type) {
case nil:
return []aiobservabilitytypes.Message{}
case string:
if strings.TrimSpace(v) == "" {
return []aiobservabilitytypes.Message{}
}
original = v
if err := json.Unmarshal([]byte(v), &value); err != nil {
return []aiobservabilitytypes.Message{{Content: []aiobservabilitytypes.Part{textPart(v)}}}
}
default:
value = v
original = stringOf(v)
}
if value == nil {
return []aiobservabilitytypes.Message{}
}
if list, ok := value.([]any); ok {
if len(list) == 0 {
return []aiobservabilitytypes.Message{}
}
// [[...]]: some SDKs wrap the conversation in one more list
if _, nested := firstItem(list).([]any); nested {
value = flattenOnce(list)
}
// ["{...}", "{...}"]: an array attribute holding one JSON message per element
if decoded, ok := decodeJSONStrings(list); ok {
value = decoded
}
}
for _, convert := range converters {
if messages, ok := convert(value); ok {
return messages
}
}
return genericMessages(original)
}
type converter func(value any) (messages []aiobservabilitytypes.Message, ok bool)

View File

@@ -1,146 +0,0 @@
package genaimessages
import (
"encoding/json"
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
)
func TestNormalize(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "ListNestedInAnotherList_Unwrapped",
raw: `[[{"role":"user","content":"Hi"}]]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
{
name: "ListOfToolDefinitions_FallsBackToGeneric",
raw: `[{"type":"function","function":{"name":"get_weather","description":"Weather","parameters":{"type":"object"}}}]`,
want: []aiobservabilitytypes.Message{
{
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `[{"type":"function","function":{"name":"get_weather","description":"Weather","parameters":{"type":"object"}}}]`},
},
},
},
},
{
name: "ListOfMessagesAsJSONStrings_EachDecoded",
raw: []any{`{"role":"user","content":"Hi"}`, `{"role":"assistant","content":"Hello"}`},
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hello"},
},
},
},
},
{
name: "TextThatIsNotJSON_ReturnedAsOneTextPart",
raw: "Let the cost of the ball be x dollars.",
want: []aiobservabilitytypes.Message{
{
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Let the cost of the ball be x dollars."},
},
},
},
},
{
name: "JSONInNoKnownFormat_ReturnedVerbatimAsGeneric",
raw: `{"output": "{\"query\": \"SigNoz\"}", "kwargs": {"name": "search_web"}}`,
want: []aiobservabilitytypes.Message{
{
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"output": "{\"query\": \"SigNoz\"}", "kwargs": {"name": "search_web"}}`},
},
},
},
},
{
name: "AlreadyDecodedValue_ConvertedLikeJSON",
raw: []any{map[string]any{"role": "user", "content": "hi"}},
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "hi"},
},
},
},
},
{
name: "BlankString_ReturnsNoMessages",
raw: " \n\t ",
want: []aiobservabilitytypes.Message{},
},
{
name: "JSONNull_ReturnsNoMessages",
raw: `null`,
want: []aiobservabilitytypes.Message{},
},
})
}
func FuzzNormalize(f *testing.F) {
for _, seed := range []string{
`[{"role": "user", "parts": [{"type": "text", "content": "Hi"}]}]`,
`[{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_1", "name": "f", "input": {}}]}]`,
`[{"role": "tool", "tool_call_id": "call_1", "content": [{"type": "text", "text": "ok"}]}]`,
`{"messages": [{"role": "user", "content": "Hi"}], "system": [{"type": "text", "text": "Be brief."}]}`,
`{"choices": [{"message": {"role": "assistant", "content": "Hi"}, "finish_reason": "stop"}]}`,
`{"output": [{"type": "function_call", "call_id": "c", "name": "f", "arguments": "{}"}, {"type": "reasoning"}]}`,
`{"candidates": [{"content": {"parts": [{"functionCall": {"name": "f", "args": {}}}], "role": "model"}}]}`,
`{"generations": [[{"text": "Hi", "message": {"lc": 1, "type": "constructor", "id": ["AIMessage"], "kwargs": {}}}]]}`,
`[[{"role": "user", "content": "Hi"}]]`,
`["{\"role\": \"user\", \"content\": \"Hi\"}"]`,
``, `null`, `{}`, `[]`, `[{}]`, `"text"`, `42`,
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, raw string) {
inputs := []any{raw}
var decoded any
if json.Unmarshal([]byte(raw), &decoded) == nil {
inputs = append(inputs, decoded)
}
for _, input := range inputs {
for i, message := range Normalize(input) {
if message.Content == nil {
t.Fatalf("message %d has nil content for %#v", i, input)
}
}
}
})
}
type normalizeCase struct {
name string
raw any
want []aiobservabilitytypes.Message
}
func assertNormalize(t *testing.T, testCases []normalizeCase) {
t.Helper()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.want, Normalize(testCase.raw))
})
}
}

View File

@@ -1,254 +0,0 @@
package genaimessages
import (
"cmp"
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// chatMessage returns nil for LangGraph tool definitions.
func chatMessage(value any, defaultRole aiobservabilitytypes.MessageRole) []aiobservabilitytypes.Message {
m, ok := toObject(value)
if !ok {
return genericMessages(stringOf(value))
}
if inner, role, ok := langChainMessage(m); ok {
return chatMessage(inner, role)
}
if isLangGraphToolDefinition(m) {
return nil
}
typ := m.str("type")
if isResponsesItemType(typ) {
return responsesItemMessages(m, typ)
}
if typ == "reasoning" {
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: reasoningParts(m)}}
}
role := cmp.Or(normalizeRole(m.str("role")), standardRole(typ), defaultRole)
msg := aiobservabilitytypes.Message{
Role: role,
Content: append(messageContentParts(m, role), messageToolCallParts(m)...),
FinishReason: normalizeFinishReason(finishReasonOf(m)),
}
if refusal := m.str("refusal"); refusal != "" {
msg.Content = append(msg.Content, textPart(refusal))
}
return []aiobservabilitytypes.Message{msg}
}
// langChainMessage unwraps a serialised message: {lc, type: "constructor", id: [..., "HumanMessage"], kwargs}.
func langChainMessage(m object) (object, aiobservabilitytypes.MessageRole, bool) {
kwargs, ok := m.obj("kwargs")
if !ok || m.str("type") != "constructor" {
return nil, "", false
}
return kwargs, cmp.Or(langChainRole(m.at("id")), standardRole(kwargs.str("type"))), true
}
func langChainRole(id any) aiobservabilitytypes.MessageRole {
path, ok := id.([]any)
if !ok || len(path) == 0 {
return ""
}
switch class := stringOf(lastItem(path)); {
case strings.HasPrefix(class, "System"):
return aiobservabilitytypes.MessageRoleSystem
case strings.HasPrefix(class, "Human"):
return aiobservabilitytypes.MessageRoleUser
case strings.HasPrefix(class, "AI"):
return aiobservabilitytypes.MessageRoleAssistant
case strings.HasPrefix(class, "Tool"), strings.HasPrefix(class, "Function"):
return aiobservabilitytypes.MessageRoleTool
}
return ""
}
// isLangGraphToolDefinition matches {role: "tool", content: {type: "function"}} without tool_call_id.
func isLangGraphToolDefinition(m object) bool {
if normalizeRole(m.str("role")) != aiobservabilitytypes.MessageRoleTool {
return false
}
if has := m.has("tool_call_id"); has {
return false
}
content, ok := m.obj("content")
if !ok || content.str("type") != "function" {
return false
}
ok = content.has("function")
return ok
}
// messageContentParts turns a tool message's text into its result.
func messageContentParts(m object, role aiobservabilitytypes.MessageRole) []aiobservabilitytypes.Part {
parts := []aiobservabilitytypes.Part{}
switch content := m.at("content").(type) {
case nil:
case string:
if role == aiobservabilitytypes.MessageRoleTool {
parts = append(parts, toolMessageResult(m, content))
} else if content != "" {
parts = append(parts, textPart(content))
}
case []any:
for _, item := range content {
part := contentBlockPart(item)
if role == aiobservabilitytypes.MessageRoleTool && part.Type == aiobservabilitytypes.PartTypeText {
part = toolMessageResult(m, part.Content)
}
parts = append(parts, part)
}
case map[string]any:
if contentParts, ok := object(content).list("parts"); ok {
for _, p := range contentParts {
parts = append(parts, semconvPart(p))
}
} else if role == aiobservabilitytypes.MessageRoleTool {
parts = append(parts, toolMessageResult(m, stringOf(content)))
} else {
parts = append(parts, genericPart(content))
}
default:
parts = append(parts, genericPart(content))
}
return parts
}
func messageToolCallParts(m object) []aiobservabilitytypes.Part {
calls, _ := m.first("tool_calls", "toolCalls").([]any)
if kwargs, ok := m.obj("additional_kwargs"); ok && len(calls) == 0 {
calls, _ = kwargs.list("tool_calls")
}
parts := make([]aiobservabilitytypes.Part, 0, len(calls)+1)
for _, call := range calls {
parts = append(parts, toolCallPart(call))
}
if call, ok := m.obj("function_call"); ok {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolCall, Name: call.str("name"), Arguments: parseArguments(call.at("arguments"))})
}
return parts
}
func toolMessageResult(m object, content string) aiobservabilitytypes.Part {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: stringOf(m.first("tool_call_id", "toolCallId")), Name: m.str("name"), Content: content}
}
// semconvMessage falls back to chatMessage when m has no parts. LiteLLM mixes in
// OpenAI fields: tool_calls beside parts, and tool_call_id on tool messages.
func semconvMessage(m object, defaultRole aiobservabilitytypes.MessageRole) []aiobservabilitytypes.Message {
parts, ok := m.list("parts")
if !ok {
return chatMessage(m, defaultRole)
}
role := cmp.Or(normalizeRole(m.str("role")), defaultRole)
msg := aiobservabilitytypes.Message{
Role: role,
Content: []aiobservabilitytypes.Part{},
FinishReason: normalizeFinishReason(finishReasonOf(m)),
}
for _, p := range parts {
part := semconvPart(p)
if role == aiobservabilitytypes.MessageRoleTool && part.Type == aiobservabilitytypes.PartTypeText {
part = toolMessageResult(m, part.Content)
}
msg.Content = append(msg.Content, part)
}
msg.Content = append(msg.Content, messageToolCallParts(m)...)
return []aiobservabilitytypes.Message{msg}
}
// isResponsesItemType matches role-less Responses API tool and MCP items.
func isResponsesItemType(typ string) bool {
switch typ {
case "":
return false
case "function_call", "function_call_output", "tool_call", "custom_tool_call", "custom_tool_call_output",
"mcp_call", "mcp_list_tools", "mcp_approval_request", "mcp_approval_response":
return true
}
return strings.HasSuffix(typ, "_call") || strings.HasSuffix(typ, "_call_output")
}
// responsesItemMessages turns a built-in tool item into a call, plus its result when the item carries one.
func responsesItemMessages(m object, typ string) []aiobservabilitytypes.Message {
switch typ {
case "function_call", "tool_call", "custom_tool_call":
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{{
Type: aiobservabilitytypes.PartTypeToolCall,
ID: stringOf(m.first("call_id", "id")),
Name: m.str("name"),
Arguments: parseArguments(m.first("arguments", "args", "input")),
}}}}
case "function_call_output", "custom_tool_call_output":
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleTool, Content: []aiobservabilitytypes.Part{{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: stringOf(m.first("call_id", "id")),
Content: stringOf(m.first("output", "result")),
}}}}
}
id := stringOf(m.first("call_id", "id"))
if strings.HasSuffix(typ, "_output") || typ == "mcp_approval_response" {
return []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleTool, Content: []aiobservabilitytypes.Part{{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: id,
Name: strings.TrimSuffix(typ, "_output"),
Content: stringOf(m.first("output", "result", "results")),
}}}}
}
args := make(map[string]any, len(m))
for k, v := range m {
switch k {
case "type", "id", "call_id", "status", "name", "server_label", "output", "result", "results":
default:
args[k] = v
}
}
name := stringOf(m.first("name", "server_label"))
if name == "" {
name = typ
}
call := aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolCall, ID: id, Name: name}
if arguments, ok := m.get("arguments"); ok {
call.Arguments = parseArguments(arguments)
} else if len(args) > 0 {
call.Arguments = args
}
msg := aiobservabilitytypes.Message{Role: aiobservabilitytypes.MessageRoleAssistant, Content: []aiobservabilitytypes.Part{call}}
if result := m.first("output", "result", "results"); result != nil {
msg.Content = append(msg.Content, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: id, Name: name, Content: stringOf(result)})
}
return []aiobservabilitytypes.Message{msg}
}
// reasoningParts returns one empty thinking part for encrypted reasoning without a summary.
func reasoningParts(m object) []aiobservabilitytypes.Part {
parts := []aiobservabilitytypes.Part{}
if summary, ok := m.list("summary"); ok {
for _, s := range summary {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(asObject(s).first("text", "content"))})
}
}
if content, ok := m.list("content"); ok {
for _, c := range content {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(asObject(c).first("text", "content"))})
}
}
if len(parts) == 0 {
parts = append(parts, aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking})
}
return parts
}
func textMessage(role aiobservabilitytypes.MessageRole, content string) aiobservabilitytypes.Message {
return aiobservabilitytypes.Message{Role: role, Content: []aiobservabilitytypes.Part{textPart(content)}}
}
func genericMessages(content string) []aiobservabilitytypes.Message {
return []aiobservabilitytypes.Message{{Content: []aiobservabilitytypes.Part{genericPart(content)}}}
}

View File

@@ -1,257 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func TestNormalizeMessageChat(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "AnthropicMessages_ThinkingAndToolUseBlocksConverted",
raw: `[
{"role": "user", "content": "Hi"},
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Let me see"},
{"type": "redacted_thinking", "data": "x"},
{
"type": "tool_use",
"id": "toolu_1",
"name": "lookup",
"input": {"q": "a"}
}
],
"stop_reason": "tool_use"
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "found",
"is_error": true
}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
FinishReason: aiobservabilitytypes.FinishReasonToolCall,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Let me see"},
{Type: aiobservabilitytypes.PartTypeThinking},
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "toolu_1", Name: "lookup", Arguments: map[string]any{"q": "a"}},
},
},
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "toolu_1", Content: "found", IsError: true},
},
},
},
},
{
name: "OpenAIToolMessageTextBlocks_BecomeToolResult",
raw: `[
{
"role": "tool",
"tool_call_id": "c1",
"content": [{"type": "text", "text": "20C"}, {"type": "text", "text": "clear"}]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "c1", Content: "20C"},
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "c1", Content: "clear"},
},
},
},
},
{
name: "OpenAIRefusal_BecomesAssistantText",
raw: `[{"role": "assistant", "content": null, "refusal": "I can't help with that."}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "I can't help with that."},
},
},
},
},
{
name: "OpenAILegacyFunctionCall_BecomesToolCall",
raw: `[{"role": "assistant", "content": null, "function_call": {"name": "get_weather", "arguments": "{\"city\": \"Paris\"}"}}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, Name: "get_weather", Arguments: map[string]any{"city": "Paris"}},
},
},
},
},
})
}
func TestNormalizeMessageLangChain(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "LangChainTypedMessages_ToolCallsReadFromAdditionalKwargs",
raw: `[
{"type": "human", "content": "Weather?"},
{
"type": "ai",
"content": "",
"additional_kwargs": {
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}
}
]
}
},
{
"type": "tool",
"content": "20C",
"tool_call_id": "call_1",
"name": "get_weather"
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Weather?"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "call_1", Name: "get_weather", Arguments: map[string]any{"city": "Paris"}},
},
},
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "call_1", Name: "get_weather", Content: "20C"},
},
},
},
},
{
name: "LangGraphToolDefinitionMessage_IsDropped",
raw: `[
{
"role": "tool",
"content": {
"type": "function",
"function": {"name": "get_weather", "parameters": {}}
}
},
{"role": "user", "content": "Hi"}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Hi"},
},
},
},
},
})
}
func TestNormalizeMessageOpenAIResponsesItems(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "BuiltInToolCallItem_BecomesToolCall",
raw: `{
"object": "response",
"status": "completed",
"output": [
{
"type": "web_search_call",
"id": "ws_1",
"status": "completed",
"action": {"type": "search", "query": "SigNoz"}
},
{
"type": "custom_tool_call",
"call_id": "c1",
"name": "grep",
"input": "foo"
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Found it."}]
}
]
}`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "ws_1", Name: "web_search_call", Arguments: map[string]any{"action": map[string]any{"type": "search", "query": "SigNoz"}}},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "c1", Name: "grep", Arguments: "foo"},
},
},
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeText, Content: "Found it."},
},
FinishReason: aiobservabilitytypes.FinishReasonStop,
},
},
},
{
name: "BuiltInToolOutputItem_BecomesToolResult",
raw: `[{"type": "local_shell_call_output", "call_id": "call_2", "output": "ok"}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "call_2", Name: "local_shell_call", Content: "ok"},
},
},
},
},
{
name: "MCPCallItemWithOutput_BecomesToolCallAndToolResult",
raw: `[{"type": "mcp_call", "id": "mcp_1", "server_label": "deepwiki", "name": "ask_question", "arguments": "{\"q\": \"What is SigNoz?\"}", "output": "An observability platform."}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "mcp_1", Name: "ask_question", Arguments: map[string]any{"q": "What is SigNoz?"}},
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "mcp_1", Name: "ask_question", Content: "An observability platform."},
},
},
},
},
})
}

View File

@@ -1,152 +0,0 @@
package genaimessages
import (
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func contentBlockPart(value any) aiobservabilitytypes.Part {
p, ok := toObject(value)
if !ok {
if s, ok := value.(string); ok {
return textPart(s)
}
return genericPart(value)
}
typ := p.str("type")
switch typ {
case "text", "input_text", "output_text", "refusal", "summary_text":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: stringOf(p.first("text", "content", "refusal"))}
case "thinking", "reasoning":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(p.first("thinking", "text", "content", "reasoning"))}
case "redacted_thinking":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking}
case "tool-call", "tool_use", "tool_call", "function_call", "server_tool_use", "mcp_tool_use":
return toolCallPart(p)
case "tool-result", "tool_result", "function_call_output":
return toolResultBlockPart(p, p.first("result", "output", "content"))
case "":
if text, ok := p.get("text"); ok {
return textPart(stringOf(text))
}
default:
if strings.HasSuffix(typ, "_tool_result") {
return toolResultBlockPart(p, p.at("content"))
}
}
return genericPart(p)
}
func semconvPart(value any) aiobservabilitytypes.Part {
p, ok := toObject(value)
if !ok {
if s, ok := value.(string); ok {
return textPart(s)
}
return genericPart(value)
}
switch p.str("type") {
case "text":
if p.flag("thought") {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(p.first("content", "text"))}
}
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: stringOf(p.first("content", "text"))}
case "reasoning", "thinking":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(p.first("content", "thinking", "text"))}
case "redacted_thinking", "redacted_reasoning":
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking}
case "tool_call":
return aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolCall,
ID: p.str("id"),
Name: p.str("name"),
Arguments: parseArguments(p.first("arguments", "args", "input")),
}
case "tool_call_response":
return aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: p.str("id"),
Name: p.str("name"),
Content: stringOf(p.first("response", "result", "content", "output")),
IsError: boolOf(p.first("is_error", "isError")),
}
case "":
// Gemini parts carry no type; the field name is the type.
if text, ok := p.get("text"); ok {
if p.flag("thought") {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeThinking, Content: stringOf(text)}
}
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: stringOf(text)}
}
if call, ok := toObject(p.first("functionCall", "function_call")); ok {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolCall, ID: call.str("id"), Name: call.str("name"), Arguments: parseArguments(call.at("args"))}
}
if resp, ok := toObject(p.first("functionResponse", "function_response")); ok {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: resp.str("id"), Name: resp.str("name"), Content: resp.str("response")}
}
}
return genericPart(p)
}
// toolCallPart reads the OpenAI, flat, Anthropic and Vercel tool call shapes.
func toolCallPart(value any) aiobservabilitytypes.Part {
call, ok := toObject(value)
if !ok {
return genericPart(value)
}
part := aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolCall,
ID: stringOf(call.first("toolCallId", "call_id", "id")),
Name: stringOf(call.first("toolName", "name")),
}
if fn, ok := call.obj("function"); ok {
part.Name = fn.str("name")
part.Arguments = parseArguments(fn.at("arguments"))
return part
}
part.Arguments = parseArguments(call.first("arguments", "args", "input"))
return part
}
// toolResultBlockPart unwraps the Vercel {type, value} result wrapper.
func toolResultBlockPart(p object, result any) aiobservabilitytypes.Part {
if nested, ok := toObject(result); ok && len(nested) <= 2 {
if v, ok := nested.get("value"); ok {
result = v
}
}
return aiobservabilitytypes.Part{
Type: aiobservabilitytypes.PartTypeToolResult,
ToolCallID: stringOf(p.first("toolCallId", "tool_use_id", "tool_call_id", "call_id", "id")),
Name: stringOf(p.first("toolName", "name")),
Content: toolResultText(result),
IsError: boolOf(p.first("isError", "is_error")),
}
}
// toolResultText joins a list of text blocks; anything else goes through stringOf.
func toolResultText(result any) string {
list, ok := result.([]any)
if !ok || len(list) == 0 {
return stringOf(result)
}
texts := make([]string, 0, len(list))
for _, item := range list {
block := asObject(item)
text, ok := block.text("text")
if !ok || (len(block) == 2 && block.str("type") != "text") || len(block) > 2 {
return stringOf(result)
}
texts = append(texts, text)
}
return strings.Join(texts, "\n")
}
func textPart(content string) aiobservabilitytypes.Part {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeText, Content: content}
}
func genericPart(value any) aiobservabilitytypes.Part {
return aiobservabilitytypes.Part{Type: aiobservabilitytypes.PartTypeGeneric, Content: stringOf(value)}
}

View File

@@ -1,130 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
func TestNormalizePartContent(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "PartWithUnknownType_BecomesGeneric",
raw: `[{"role":"user","parts":[{"type":"image","url":"http://x/y.png"}]}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeGeneric, Content: `{"type":"image","url":"http://x/y.png"}`},
},
},
},
},
})
}
func TestNormalizePartThinking(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "ThoughtReasoningAndHiddenParts_BecomeThinking",
raw: `[
{
"role": "assistant",
"parts": [
{"type": "text", "content": "Let me think", "thought": true},
{"type": "reasoning", "content": "step 1"},
{"type": "redacted_reasoning", "data": "xyz"},
{"type": "text", "content": "Answer"}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeThinking, Content: "Let me think"},
{Type: aiobservabilitytypes.PartTypeThinking, Content: "step 1"},
{Type: aiobservabilitytypes.PartTypeThinking},
{Type: aiobservabilitytypes.PartTypeText, Content: "Answer"},
},
},
},
},
})
}
func TestNormalizePartTools(t *testing.T) {
assertNormalize(t, []normalizeCase{
{
name: "AnthropicServerToolUseAndWebSearchResult_BecomeToolCallAndResult",
raw: `[
{
"role": "assistant",
"content": [
{"type": "server_tool_use", "id": "srvtoolu_1", "name": "web_search", "input": {"query": "SigNoz"}},
{"type": "web_search_tool_result", "tool_use_id": "srvtoolu_1", "content": [{"type": "web_search_result", "title": "SigNoz", "url": "https://signoz.io"}]}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "srvtoolu_1", Name: "web_search", Arguments: map[string]any{"query": "SigNoz"}},
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "srvtoolu_1", Content: `[{"title":"SigNoz","type":"web_search_result","url":"https://signoz.io"}]`},
},
},
},
},
{
name: "AnthropicToolResultTextBlocks_JoinedIntoOneString",
raw: `[
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "t1",
"content": [
{"type": "text", "text": "line one"},
{"type": "text", "text": "line two"}
]
}
]
}
]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleUser,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "t1", Content: "line one\nline two"},
},
},
},
},
{
name: "VercelToolResultJSONWrapper_ValueUnwrapped",
raw: `[{"role": "tool", "content": [{"type": "tool-result", "toolCallId": "call_1", "toolName": "get_weather", "result": {"type": "json", "value": {"temp_c": 18}}}]}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleTool,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolResult, ToolCallID: "call_1", Name: "get_weather", Content: `{"temp_c":18}`},
},
},
},
},
{
name: "ToolCallWithInvalidJSONArguments_ArgumentsKeptAsString",
raw: `[{"role": "assistant", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "get_weather", "arguments": "{city: Paris"}}]}]`,
want: []aiobservabilitytypes.Message{
{
Role: aiobservabilitytypes.MessageRoleAssistant,
Content: []aiobservabilitytypes.Part{
{Type: aiobservabilitytypes.PartTypeToolCall, ID: "call_1", Name: "get_weather", Arguments: "{city: Paris"},
},
},
},
},
})
}

View File

@@ -1,54 +0,0 @@
package genaimessages
import (
"strings"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
var finishReasonKeys = []string{"finish_reason", "finishReason", "stop_reason"}
// normalizeRole keeps an unknown role, lowercased.
func normalizeRole(role string) aiobservabilitytypes.MessageRole {
if known := standardRole(role); known != "" {
return known
}
return aiobservabilitytypes.MessageRole(strings.ToLower(strings.TrimSpace(role)))
}
// normalizeFinishReason keeps an unknown reason, lowercased.
func normalizeFinishReason(reason string) aiobservabilitytypes.FinishReason {
lowered := strings.ToLower(strings.TrimSpace(reason))
switch lowered {
case "stop", "end_turn", "stop_sequence", "completed", "complete", "eos", "finished":
return aiobservabilitytypes.FinishReasonStop
case "tool_call", "tool_calls", "tool_use", "function_call":
return aiobservabilitytypes.FinishReasonToolCall
case "length", "max_tokens", "max_output_tokens", "max_completion_tokens", "model_length":
return aiobservabilitytypes.FinishReasonLength
case "content_filter", "content_filtered", "guardrail_intervened", "safety", "refusal", "recitation", "blocklist", "prohibited_content", "spii":
return aiobservabilitytypes.FinishReasonContentFilter
case "error", "failed", "incomplete":
return aiobservabilitytypes.FinishReasonError
}
return aiobservabilitytypes.FinishReason(lowered)
}
// standardRole maps vendor role names onto MessageRole; anything else is "".
func standardRole(role string) aiobservabilitytypes.MessageRole {
switch strings.ToLower(strings.TrimSpace(role)) {
case "system", "developer":
return aiobservabilitytypes.MessageRoleSystem
case "user", "human":
return aiobservabilitytypes.MessageRoleUser
case "assistant", "ai", "model":
return aiobservabilitytypes.MessageRoleAssistant
case "tool", "function":
return aiobservabilitytypes.MessageRoleTool
}
return ""
}
func finishReasonOf(m object) string {
return stringOf(m.first(finishReasonKeys...))
}

View File

@@ -1,53 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
)
func TestNormalizeRole(t *testing.T) {
testCases := []struct {
name string
roles []string
want aiobservabilitytypes.MessageRole
}{
{name: "SystemAndDeveloper_MapToSystem", roles: []string{"system", "developer", " Developer "}, want: aiobservabilitytypes.MessageRoleSystem},
{name: "UserAndHuman_MapToUser", roles: []string{"user", "human", "HUMAN"}, want: aiobservabilitytypes.MessageRoleUser},
{name: "AssistantAIAndModel_MapToAssistant", roles: []string{"assistant", "ai", "model"}, want: aiobservabilitytypes.MessageRoleAssistant},
{name: "ToolAndFunction_MapToTool", roles: []string{"tool", "function"}, want: aiobservabilitytypes.MessageRoleTool},
{name: "UnknownRole_KeptLowercased", roles: []string{"Narrator"}, want: "narrator"},
{name: "EmptyRole_StaysEmpty", roles: []string{"", " "}, want: ""},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
for _, role := range testCase.roles {
assert.Equal(t, testCase.want, normalizeRole(role), role)
}
})
}
}
func TestNormalizeFinishReason(t *testing.T) {
testCases := []struct {
name string
reasons []string
want aiobservabilitytypes.FinishReason
}{
{name: "EndOfAnswer_MapsToStop", reasons: []string{"stop", "end_turn", "stop_sequence", "completed", "complete", "eos", "finished", "STOP"}, want: aiobservabilitytypes.FinishReasonStop},
{name: "ToolCallVariants_MapToToolCall", reasons: []string{"tool_call", "tool_calls", "tool_use", "function_call"}, want: aiobservabilitytypes.FinishReasonToolCall},
{name: "TokenLimits_MapToLength", reasons: []string{"length", "max_tokens", "max_output_tokens", "max_completion_tokens", "model_length", "MAX_TOKENS"}, want: aiobservabilitytypes.FinishReasonLength},
{name: "SafetyBlocks_MapToContentFilter", reasons: []string{"content_filter", "content_filtered", "guardrail_intervened", "safety", "refusal", "recitation", "blocklist", "prohibited_content", "spii", "SAFETY"}, want: aiobservabilitytypes.FinishReasonContentFilter},
{name: "Failures_MapToError", reasons: []string{"error", "failed", "incomplete"}, want: aiobservabilitytypes.FinishReasonError},
{name: "UnknownReason_KeptLowercased", reasons: []string{"Weird"}, want: "weird"},
{name: "EmptyReason_StaysEmpty", reasons: []string{"", " "}, want: ""},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
for _, reason := range testCase.reasons {
assert.Equal(t, testCase.want, normalizeFinishReason(reason), reason)
}
})
}
}

View File

@@ -1 +0,0 @@
[{"role":"user","content":"What's the weather in Bengaluru? Use the tool."},{"role":"assistant","content":"","tool_calls":[{"id":"call_yGnJsGOZMnHKTA2x1XM8897b","type":"function","name":"get_current_weather","args":"{\"city\":\"Bengaluru\"}"}]},{"role":"tool","content":"{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]

View File

@@ -1,33 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_yGnJsGOZMnHKTA2x1XM8897b",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role":"assistant","content":"","tool_calls":[{"id":"call_yGnJsGOZMnHKTA2x1XM8897b","type":"function","name":"get_current_weather","args":"{\"city\":\"Bengaluru\"}"}]}]

View File

@@ -1,15 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_yGnJsGOZMnHKTA2x1XM8897b",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_iXrmKomEbGycID5aX4BYrQcg", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_iXrmKomEbGycID5aX4BYrQcg", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_iXrmKomEbGycID5aX4BYrQcg",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_iXrmKomEbGycID5aX4BYrQcg"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_iXrmKomEbGycID5aX4BYrQcg", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_iXrmKomEbGycID5aX4BYrQcg",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"type": "text", "content": "What's the weather in Bengaluru?"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "id": "call_mnRhZ0fYlvrSujkEcdmpppNw", "name": "get_weather", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_mnRhZ0fYlvrSujkEcdmpppNw", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_mnRhZ0fYlvrSujkEcdmpppNw",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_mnRhZ0fYlvrSujkEcdmpppNw"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "id": "call_mnRhZ0fYlvrSujkEcdmpppNw", "name": "get_weather", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_mnRhZ0fYlvrSujkEcdmpppNw",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
{"generations":[[{"text":"","generation_info":{"finish_reason":"tool_calls","logprobs":null},"type":"ChatGeneration","message":{"lc":1,"type":"constructor","id":["langchain","schema","messages","AIMessage"],"kwargs":{"content":"","additional_kwargs":{"refusal":null},"response_metadata":{"token_usage":{"completion_tokens":17,"prompt_tokens":63,"total_tokens":80,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"model_provider":"openai","model_name":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_5feb5fe6c2","id":"chatcmpl-Dm101OqaY01WKntqj29Epz5er05ry","service_tier":"default","finish_reason":"tool_calls","logprobs":null},"type":"ai","id":"lc_run--019e844a-d27a-7c23-b1ed-a542e0f855da-0","tool_calls":[{"name":"get_weather","args":{"city":"Bengaluru"},"id":"call_nbrbkc5fom95q8uMA2bCvH6s","type":"tool_call"}],"usage_metadata":{"input_tokens":63,"output_tokens":17,"total_tokens":80,"input_token_details":{"audio":0,"cache_read":0},"output_token_details":{"audio":0,"reasoning":0}},"invalid_tool_calls":[]}}}]],"llm_output":{"token_usage":{"completion_tokens":17,"prompt_tokens":63,"total_tokens":80,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"model_provider":"openai","model_name":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_5feb5fe6c2","id":"chatcmpl-Dm101OqaY01WKntqj29Epz5er05ry","service_tier":"default"},"run":null,"type":"LLMResult"}

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_nbrbkc5fom95q8uMA2bCvH6s",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
{"messages":[[{"lc":1,"type":"constructor","id":["langchain","schema","messages","SystemMessage"],"kwargs":{"content":"You are a weather assistant. Call get_weather once per city.","type":"system"}},{"lc":1,"type":"constructor","id":["langchain","schema","messages","HumanMessage"],"kwargs":{"content":"What's the weather in Bengaluru?","type":"human"}},{"lc":1,"type":"constructor","id":["langchain","schema","messages","AIMessage"],"kwargs":{"content":"","additional_kwargs":{"refusal":null},"response_metadata":{"token_usage":{"completion_tokens":17,"prompt_tokens":63,"total_tokens":80,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}},"model_provider":"openai","model_name":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_5feb5fe6c2","id":"chatcmpl-Dm101OqaY01WKntqj29Epz5er05ry","service_tier":"default","finish_reason":"tool_calls","logprobs":null},"type":"ai","id":"lc_run--019e844a-d27a-7c23-b1ed-a542e0f855da-0","tool_calls":[{"name":"get_weather","args":{"city":"Bengaluru"},"id":"call_nbrbkc5fom95q8uMA2bCvH6s","type":"tool_call"}],"usage_metadata":{"input_tokens":63,"output_tokens":17,"total_tokens":80,"input_token_details":{"audio":0,"cache_read":0},"output_token_details":{"audio":0,"reasoning":0}},"invalid_tool_calls":[]}},{"lc":1,"type":"constructor","id":["langchain","schema","messages","ToolMessage"],"kwargs":{"content":"{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}","type":"tool","tool_call_id":"call_nbrbkc5fom95q8uMA2bCvH6s","status":"success"}}]]}

View File

@@ -1,43 +0,0 @@
[
{
"role": "system",
"content": [
{
"type": "text",
"content": "You are a weather assistant. Call get_weather once per city."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_nbrbkc5fom95q8uMA2bCvH6s",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_nbrbkc5fom95q8uMA2bCvH6s"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"type": "text", "content": "What's the weather in Bengaluru? Use the tool."}]}, {"role": "assistant", "parts": [], "tool_calls": [{"function": {"arguments": "{\"city\":\"Bengaluru\"}", "name": "get_current_weather"}, "id": "call_GuU9siyYfjnx3DfHKzFbRrvD", "type": "function"}]}, {"role": "tool", "parts": [{"type": "text", "content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}], "tool_call_id": "call_GuU9siyYfjnx3DfHKzFbRrvD"}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_GuU9siyYfjnx3DfHKzFbRrvD",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_GuU9siyYfjnx3DfHKzFbRrvD"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "text", "content": "The animal in the image is a cartoon duck."}], "finish_reason": "stop"}]

View File

@@ -1,12 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "text",
"content": "The animal in the image is a cartoon duck."
}
],
"finishReason": "stop"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_itnlpgQcfe0fb0nLJym7DTnt", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_itnlpgQcfe0fb0nLJym7DTnt", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_itnlpgQcfe0fb0nLJym7DTnt",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_itnlpgQcfe0fb0nLJym7DTnt"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_itnlpgQcfe0fb0nLJym7DTnt", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_itnlpgQcfe0fb0nLJym7DTnt",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role":"system","content":"You are a weather assistant. When asked about weather, call get-weather once per city and summarise."},{"role":"user","content":[{"type":"text","text":"What is the weather in Bengaluru?"}]},{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_526phoykytobsvLouzES805J","toolName":"getWeather","args":{"city":"Bengaluru"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_526phoykytobsvLouzES805J","toolName":"getWeather","result":{"city":"Bengaluru","temperatureC":18,"summary":"Clear"}}]}]

View File

@@ -1,44 +0,0 @@
[
{
"role": "system",
"content": [
{
"type": "text",
"content": "You are a weather assistant. When asked about weather, call get-weather once per city and summarise."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"content": "What is the weather in Bengaluru?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_526phoykytobsvLouzES805J",
"name": "getWeather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\":\"Bengaluru\",\"summary\":\"Clear\",\"temperatureC\":18}",
"name": "getWeather",
"toolCallId": "call_526phoykytobsvLouzES805J"
}
]
}
]

View File

@@ -1 +0,0 @@
The current weather in Bengaluru is clear with a temperature of 18°C.

View File

@@ -1,10 +0,0 @@
[
{
"content": [
{
"type": "text",
"content": "The current weather in Bengaluru is clear with a temperature of 18°C."
}
]
}
]

View File

@@ -1 +0,0 @@
[{"toolCallType":"function","toolCallId":"call_526phoykytobsvLouzES805J","toolName":"getWeather","args":"{\"city\":\"Bengaluru\"}"}]

View File

@@ -1,15 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_526phoykytobsvLouzES805J",
"name": "getWeather",
"arguments": {
"city": "Bengaluru"
}
}
]
}
]

View File

@@ -1 +0,0 @@
{"include": [], "input": [{"content": "What's the weather in Bangalore right now?", "role": "user"}, {"arguments": "{\"city\":\"Bangalore\"}", "call_id": "call_MQTQ0XYL95vA5k31cbCgRt5y", "name": "get_weather", "type": "function_call", "id": "fc_0368c3e62fb9ae12006a199f7c14c481a1a22444594cc574e4", "status": "completed"}, {"call_id": "call_MQTQ0XYL95vA5k31cbCgRt5y", "output": "27C, partly cloudy, light breeze from the south.", "type": "function_call_output"}], "instructions": "You are a concise weather assistant. When asked about weather, call the get_weather tool exactly once per city, then summarise the result in one sentence.", "model": "gpt-4o-mini", "tools": [{"name": "get_weather", "parameters": {"properties": {"city": {"title": "City", "type": "string"}}, "required": ["city"], "title": "get_weather_args", "type": "object", "additionalProperties": false}, "strict": true, "type": "function", "description": "Look up the current weather for a city. Returns a short summary string."}]}

View File

@@ -1,43 +0,0 @@
[
{
"role": "system",
"content": [
{
"type": "text",
"content": "You are a concise weather assistant. When asked about weather, call the get_weather tool exactly once per city, then summarise the result in one sentence."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bangalore right now?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_MQTQ0XYL95vA5k31cbCgRt5y",
"name": "get_weather",
"arguments": {
"city": "Bangalore"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "27C, partly cloudy, light breeze from the south.",
"toolCallId": "call_MQTQ0XYL95vA5k31cbCgRt5y"
}
]
}
]

View File

@@ -1 +0,0 @@
{"id":"resp_017c5eb995d99968006a199f9b483481979ca790db905506fc","created_at":1780064155.0,"error":null,"incomplete_details":null,"instructions":"You solve problems step by step and give a final answer.","metadata":{},"model":"o3-mini-2025-01-31","object":"response","output":[{"id":"rs_017c5eb995d99968006a199f9bff248197978bf9193f34c29a","summary":[],"type":"reasoning"},{"id":"msg_017c5eb995d99968006a199f9c8dcc81979f4e10afbe5350a1","content":[{"annotations":[],"text":"Step 1: Let the cost of the ball be x dollars.\nStep 2: Since the bat costs $1 more than the ball, its cost is x + 1 dollars.\nStep 3: According to the problem, the total cost of the bat and ball is $1.10. Set up the equation:\n  x + (x + 1) = 1.10\nStep 4: Simplify the equation:\n  2x + 1 = 1.10\nStep 5: Subtract 1 from both sides:\n  2x = 0.10\nStep 6: Divide both sides by 2:\n  x = 0.05\n\nFinal Answer: The ball costs $0.05.","type":"output_text","logprobs":[]}],"role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"temperature":1.0,"tool_choice":"auto","tools":[],"top_p":1.0,"background":false,"completed_at":1780064157.0,"max_output_tokens":null,"max_tool_calls":null,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":"medium","summary":null,"context":"current_turn"},"safety_identifier":null,"service_tier":"default","status":"completed","text":{"format":{"type":"text"},"verbosity":"medium"},"top_logprobs":0,"truncation":"disabled","usage":{"input_tokens":49,"input_tokens_details":{"cached_tokens":0},"output_tokens":431,"output_tokens_details":{"reasoning_tokens":128},"total_tokens":480},"user":null,"billing":{"payer":"developer"},"frequency_penalty":0.0,"moderation":null,"presence_penalty":0.0,"store":true}

View File

@@ -1,20 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "thinking"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"content": "Step 1: Let the cost of the ball be x dollars.\nStep 2: Since the bat costs $1 more than the ball, its cost is x + 1 dollars.\nStep 3: According to the problem, the total cost of the bat and ball is $1.10. Set up the equation:\n  x + (x + 1) = 1.10\nStep 4: Simplify the equation:\n  2x + 1 = 1.10\nStep 5: Subtract 1 from both sides:\n  2x = 0.10\nStep 6: Divide both sides by 2:\n  x = 0.05\n\nFinal Answer: The ball costs $0.05."
}
],
"finishReason": "stop"
}
]

View File

@@ -1 +0,0 @@
{"messages": [{"role": "user", "content": "What's the weather in Paris and in London? Use the tool for each."}, {"content": null, "refusal": null, "role": "assistant", "annotations": [], "audio": null, "function_call": null, "tool_calls": [{"id": "call_SW4hst7nzysgvWSAz4OdyK9D", "function": {"arguments": "{\"city\": \"Paris\"}", "name": "get_current_weather"}, "type": "function"}, {"id": "call_si8wiETYGpooMB7gMXSgMQpk", "function": {"arguments": "{\"city\": \"London\"}", "name": "get_current_weather"}, "type": "function"}]}, {"role": "tool", "tool_call_id": "call_SW4hst7nzysgvWSAz4OdyK9D", "content": "{\"city\": \"Paris\", \"temp_c\": 18, \"summary\": \"Clear\"}"}, {"role": "tool", "tool_call_id": "call_si8wiETYGpooMB7gMXSgMQpk", "content": "{\"city\": \"London\", \"temp_c\": 18, \"summary\": \"Clear\"}"}], "model": "gpt-4o-mini", "max_tokens": 80}

View File

@@ -1,52 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Paris and in London? Use the tool for each."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_SW4hst7nzysgvWSAz4OdyK9D",
"name": "get_current_weather",
"arguments": {
"city": "Paris"
}
},
{
"type": "tool_call",
"id": "call_si8wiETYGpooMB7gMXSgMQpk",
"name": "get_current_weather",
"arguments": {
"city": "London"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Paris\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_SW4hst7nzysgvWSAz4OdyK9D"
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"London\", \"temp_c\": 18, \"summary\": \"Clear\"}",
"toolCallId": "call_si8wiETYGpooMB7gMXSgMQpk"
}
]
}
]

View File

@@ -1 +0,0 @@
{"id":"chatcmpl-DksB6cYbMPwTBkJixOAuxKGZpQKds","choices":[{"finish_reason":"tool_calls","index":0,"logprobs":null,"message":{"content":null,"refusal":null,"role":"assistant","annotations":[],"tool_calls":[{"id":"call_MdYKTRrkshuBG9W5oBXYZaRc","function":{"arguments":"{\"city\":\"Bengaluru\",\"unit\":\"c\"}","name":"get_current_weather"},"type":"function"}]}}],"created":1780063720,"model":"gpt-4o-mini-2024-07-18","object":"chat.completion","service_tier":"default","system_fingerprint":"fp_c6aa0586c9","usage":{"completion_tokens":22,"prompt_tokens":65,"total_tokens":87,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}}}

View File

@@ -1,17 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_MdYKTRrkshuBG9W5oBXYZaRc",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru",
"unit": "c"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_weather", "id": "call_TaqNOsbmsz4ifWLVyDHYAAEK", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_TaqNOsbmsz4ifWLVyDHYAAEK", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_TaqNOsbmsz4ifWLVyDHYAAEK",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_TaqNOsbmsz4ifWLVyDHYAAEK"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_weather", "id": "call_TaqNOsbmsz4ifWLVyDHYAAEK", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_TaqNOsbmsz4ifWLVyDHYAAEK",
"name": "get_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
{"completion":"Let x be the cost of the ball. Then the bat costs x + 1 dollars. The total cost is given as:\n\n  x + (x + 1) = 1.10\n\nCombine like terms:\n\n  2x + 1 = 1.10\n\nSubtract 1 from both sides:\n\n  2x = 0.10\n\nDivide both sides by 2:\n\n  x = 0.05\n\nSo, the ball costs 5 cents.","reasoning":"**Calculating ball price**\n\nI'm determining the price of the ball based on a scenario where a bat and ball combined cost $1.10, with the bat costing $1 more than the ball. I set up the equation: let the cost of the ball be x, making the bat x + 1. Solving the equation x + (x + 1) = 1.10 leads me to find that x equals $0.05 for the ball. I need to confirm that this works: the ball at $0.05 and the bat at $1.05 totals $1.10.","rawRequest":{"model":"openai/o3-mini","max_completion_tokens":2000}}

View File

@@ -1,15 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "thinking",
"content": "**Calculating ball price**\n\nI'm determining the price of the ball based on a scenario where a bat and ball combined cost $1.10, with the bat costing $1 more than the ball. I set up the equation: let the cost of the ball be x, making the bat x + 1. Solving the equation x + (x + 1) = 1.10 leads me to find that x equals $0.05 for the ball. I need to confirm that this works: the ball at $0.05 and the bat at $1.05 totals $1.10."
},
{
"type": "text",
"content": "Let x be the cost of the ball. Then the bat costs x + 1 dollars. The total cost is given as:\n\n  x + (x + 1) = 1.10\n\nCombine like terms:\n\n  2x + 1 = 1.10\n\nSubtract 1 from both sides:\n\n  2x = 0.10\n\nDivide both sides by 2:\n\n  x = 0.05\n\nSo, the ball costs 5 cents."
}
]
}
]

View File

@@ -1 +0,0 @@
{"messages":[{"role":"user","content":"What's the weather in Bengaluru? Use the tool."},{"content":null,"refusal":null,"role":"assistant","annotations":null,"audio":null,"function_call":null,"tool_calls":[{"id":"call_AwiGdiFjghGVhbmbGtt92rjs","function":{"arguments":"{\"city\":\"Bengaluru\"}","name":"get_current_weather"},"type":"function","index":0}],"reasoning":null},{"role":"tool","tool_call_id":"call_AwiGdiFjghGVhbmbGtt92rjs","content":"{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_AwiGdiFjghGVhbmbGtt92rjs",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_AwiGdiFjghGVhbmbGtt92rjs"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_4BfNpuiPppRa5N1LQIkuBknW", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_4BfNpuiPppRa5N1LQIkuBknW", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_4BfNpuiPppRa5N1LQIkuBknW",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_4BfNpuiPppRa5N1LQIkuBknW"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_4BfNpuiPppRa5N1LQIkuBknW", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_4BfNpuiPppRa5N1LQIkuBknW",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role": "user", "parts": [{"content": "What's the weather in Bengaluru? Use the tool.", "type": "text"}]}, {"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_dYNkfcNlSN2A0TPAkSGXDPJb", "arguments": {"city": "Bengaluru"}}]}, {"role": "tool", "parts": [{"type": "tool_call_response", "id": "call_dYNkfcNlSN2A0TPAkSGXDPJb", "response": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}"}]}]

View File

@@ -1,34 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What's the weather in Bengaluru? Use the tool."
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_dYNkfcNlSN2A0TPAkSGXDPJb",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\": \"Bengaluru\", \"temp_c\": 28, \"summary\": \"Partly cloudy\"}",
"toolCallId": "call_dYNkfcNlSN2A0TPAkSGXDPJb"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"role": "assistant", "parts": [{"type": "tool_call", "name": "get_current_weather", "id": "call_dYNkfcNlSN2A0TPAkSGXDPJb", "arguments": {"city": "Bengaluru"}}], "finish_reason": "tool_call"}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_dYNkfcNlSN2A0TPAkSGXDPJb",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru"
}
}
],
"finishReason": "tool_call"
}
]

View File

@@ -1 +0,0 @@
[{"role":"user","content":[{"type":"text","text":"What is the weather in Bengaluru in celsius?"}]},{"role":"assistant","content":[{"type":"tool-call","toolCallId":"call_l0ycKcmt1uPCdGWR4SkwoLBv","toolName":"get_current_weather","args":{"city":"Bengaluru","unit":"c"}}]},{"role":"tool","content":[{"type":"tool-result","toolCallId":"call_l0ycKcmt1uPCdGWR4SkwoLBv","toolName":"get_current_weather","result":{"city":"Bengaluru","unit":"c","temperature":27,"condition":"partly cloudy"}}]}]

View File

@@ -1,36 +0,0 @@
[
{
"role": "user",
"content": [
{
"type": "text",
"content": "What is the weather in Bengaluru in celsius?"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_l0ycKcmt1uPCdGWR4SkwoLBv",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru",
"unit": "c"
}
}
]
},
{
"role": "tool",
"content": [
{
"type": "tool_result",
"content": "{\"city\":\"Bengaluru\",\"condition\":\"partly cloudy\",\"temperature\":27,\"unit\":\"c\"}",
"name": "get_current_weather",
"toolCallId": "call_l0ycKcmt1uPCdGWR4SkwoLBv"
}
]
}
]

View File

@@ -1 +0,0 @@
Hello! How are you today?

View File

@@ -1,10 +0,0 @@
[
{
"content": [
{
"type": "text",
"content": "Hello! How are you today?"
}
]
}
]

View File

@@ -1 +0,0 @@
[{"type":"tool-call","toolCallId":"call_l0ycKcmt1uPCdGWR4SkwoLBv","toolName":"get_current_weather","args":{"city":"Bengaluru","unit":"c"}}]

View File

@@ -1,16 +0,0 @@
[
{
"role": "assistant",
"content": [
{
"type": "tool_call",
"id": "call_l0ycKcmt1uPCdGWR4SkwoLBv",
"name": "get_current_weather",
"arguments": {
"city": "Bengaluru",
"unit": "c"
}
}
]
}
]

View File

@@ -1,160 +0,0 @@
package genaimessages
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
)
// parseArguments decodes JSON-encoded arguments; anything else is returned as is.
func parseArguments(value any) any {
s, ok := value.(string)
if !ok {
return value
}
var decoded any
if err := json.Unmarshal([]byte(s), &decoded); err != nil {
return s
}
return decoded
}
func decodeJSONStrings(list []any) ([]any, bool) {
out := make([]any, 0, len(list))
for _, item := range list {
s, ok := item.(string)
if !ok {
return nil, false
}
var decoded map[string]any
if err := json.Unmarshal([]byte(s), &decoded); err != nil || decoded == nil {
return nil, false
}
out = append(out, decoded)
}
return out, true
}
func flattenOnce(list []any) []any {
out := make([]any, 0, len(list))
for _, item := range list {
if inner, ok := item.([]any); ok {
out = append(out, inner...)
continue
}
out = append(out, item)
}
return out
}
// object is a decoded JSON object. Its accessors return zero values for missing
// keys and for values of another type, so reads never panic.
type object map[string]any
func toObject(value any) (object, bool) {
switch v := value.(type) {
case object:
return v, true
case map[string]any:
return v, true
}
return nil, false
}
func asObject(value any) object {
m, _ := toObject(value)
return m
}
func (o object) at(key string) any {
return o[key]
}
func (o object) get(key string) (any, bool) {
value, ok := o[key]
return value, ok
}
func (o object) has(key string) bool {
_, ok := o[key]
return ok
}
func (o object) str(key string) string {
return stringOf(o[key])
}
func (o object) flag(key string) bool {
return boolOf(o[key])
}
func (o object) text(key string) (string, bool) {
s, ok := o[key].(string)
return s, ok
}
func (o object) obj(key string) (object, bool) {
return toObject(o[key])
}
func (o object) list(key string) ([]any, bool) {
l, ok := o[key].([]any)
return l, ok
}
// lookup returns the first non-nil value among keys.
func (o object) lookup(keys ...string) (any, bool) {
for _, k := range keys {
if value, ok := o[k]; ok && value != nil {
return value, true
}
}
return nil, false
}
func (o object) first(keys ...string) any {
value, _ := o.lookup(keys...)
return value
}
func firstItem(list []any) any {
if len(list) == 0 {
return nil
}
return list[0]
}
// lastMessage returns nil for an empty slice.
func lastMessage(messages []aiobservabilitytypes.Message) *aiobservabilitytypes.Message {
if len(messages) == 0 {
return nil
}
return &messages[len(messages)-1]
}
func lastItem(list []any) any {
if len(list) == 0 {
return nil
}
return list[len(list)-1]
}
func boolOf(value any) bool {
b, _ := value.(bool)
return b
}
// stringOf renders nil as "" and non-strings as compact JSON.
func stringOf(value any) string {
switch v := value.(type) {
case nil:
return ""
case string:
return v
}
data, err := json.Marshal(value)
if err != nil {
return ""
}
return string(data)
}

View File

@@ -1,53 +0,0 @@
package genaimessages
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/aiobservabilitytypes"
"github.com/stretchr/testify/assert"
)
func TestObjectAccessors_NilObject_ZeroValues(t *testing.T) {
var o object
assert.Nil(t, o.at("key"))
assert.False(t, o.has("key"))
assert.Equal(t, "", o.str("key"))
assert.False(t, o.flag("key"))
assert.Nil(t, o.first("a", "b"))
_, ok := o.obj("key")
assert.False(t, ok)
_, ok = o.list("key")
assert.False(t, ok)
_, ok = o.text("key")
assert.False(t, ok)
}
func TestObjectAccessors_WrongType_ZeroValues(t *testing.T) {
o := object{"number": 1.5, "text": "hi"}
_, ok := o.obj("number")
assert.False(t, ok)
_, ok = o.list("text")
assert.False(t, ok)
_, ok = o.text("number")
assert.False(t, ok)
assert.False(t, o.flag("text"))
assert.Equal(t, "1.5", o.str("number"))
}
func TestSliceHelpers_EmptySlice_NilOrZero(t *testing.T) {
assert.Nil(t, firstItem(nil))
assert.Nil(t, lastItem([]any{}))
assert.Nil(t, lastMessage(nil))
assert.Equal(t, "b", lastItem([]any{"a", "b"}))
messages := []aiobservabilitytypes.Message{{Role: aiobservabilitytypes.MessageRoleUser}, {Role: aiobservabilitytypes.MessageRoleAssistant}}
assert.Same(t, &messages[1], lastMessage(messages))
}
func TestToObject_AcceptsDecodedMapAndObject(t *testing.T) {
_, ok := toObject(map[string]any{"a": 1})
assert.True(t, ok)
_, ok = toObject(object{"a": 1})
assert.True(t, ok)
_, ok = toObject([]any{})
assert.False(t, ok)
}

View File

@@ -1,57 +0,0 @@
package aiobservabilitytypes
const (
MessageRoleSystem MessageRole = "system"
MessageRoleUser MessageRole = "user"
MessageRoleAssistant MessageRole = "assistant"
MessageRoleTool MessageRole = "tool"
)
const (
FinishReasonStop FinishReason = "stop"
FinishReasonToolCall FinishReason = "tool_call"
FinishReasonLength FinishReason = "length"
FinishReasonContentFilter FinishReason = "content_filter"
FinishReasonError FinishReason = "error"
)
const (
PartTypeText PartType = "text"
PartTypeThinking PartType = "thinking"
PartTypeToolCall PartType = "tool_call"
PartTypeToolResult PartType = "tool_result"
PartTypeGeneric PartType = "generic"
)
type MessageRole string
type FinishReason string
type PartType string
// Part is one piece of a message. Which fields are set depends on Type:
//
// text Content
// thinking Content, empty when the provider hid the reasoning
// tool_call ID, Name, Arguments
// tool_result ToolCallID, Name, Content, IsError
// generic Content (the original value, always a string)
type Part struct {
Type PartType `json:"type" required:"true"`
Content string `json:"content,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Arguments any `json:"arguments,omitempty"`
ToolCallID string `json:"toolCallId,omitempty"`
IsError bool `json:"isError,omitempty"`
}
type Message struct {
Role MessageRole `json:"role,omitempty"`
Content []Part `json:"content" required:"true" nullable:"false"`
FinishReason FinishReason `json:"finishReason,omitempty"`
}
func (PartType) Enum() []any {
return []any{PartTypeText, PartTypeThinking, PartTypeToolCall, PartTypeToolResult, PartTypeGeneric}
}

View File

@@ -1,62 +0,0 @@
package alertmanagertypes
import (
"maps"
"net/textproto"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/alertmanager/config"
)
// ChannelEmailConfig carries no SMTP transport fields: the smarthost,
// credentials and TLS settings come from the deployment's global config, so a
// channel can only choose recipients and body.
type ChannelEmailConfig struct {
SendResolved *bool `json:"sendResolved,omitempty"`
To string `json:"to" required:"true"`
HTML valuer.UnsetOrNonEmptyString `json:"html"`
Headers map[string]string `json:"headers,omitempty"`
}
func (c ChannelEmailConfig) Validate() error {
if c.To == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.to is required for an email channel")
}
// A read reports header names as textproto canonicalizes them, turning
// "subject" into "Subject", so a name that is not already in that form is
// rejected rather than answered with one the caller never sent.
for _, header := range slices.Sorted(maps.Keys(c.Headers)) {
if canonical := textproto.CanonicalMIMEHeaderKey(header); canonical != header {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.headers name %q must be written as %q", header, canonical)
}
}
return nil
}
func (c ChannelEmailConfig) toUndefaultedReceiver(displayName string) (*Receiver, error) {
return &Receiver{Receiver: &config.Receiver{
Name: displayName,
EmailConfigs: []*config.EmailConfig{{
NotifierConfig: config.NotifierConfig{VSendResolved: resolveSendResolved(c.SendResolved, config.DefaultEmailConfig.VSendResolved)},
To: c.To,
HTML: c.HTML.StringValue(),
Headers: c.Headers,
}},
}}, nil
}
func newChannelEmailConfigFromReceiver(_ string, receiver *Receiver) (ChannelSpec, error) {
email := receiver.EmailConfigs[0]
sendResolved := email.VSendResolved
return &ChannelEmailConfig{
SendResolved: &sendResolved,
To: email.To,
HTML: valuer.UnsetIfEmpty(email.HTML),
Headers: email.Headers,
}, nil
}

View File

@@ -1,55 +0,0 @@
package alertmanagertypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/alertmanager/config"
)
type ChannelMSTeamsConfig struct {
SendResolved *bool `json:"sendResolved,omitempty"`
WebhookURL string `json:"webhookUrl" required:"true" format:"password"`
Title valuer.UnsetOrNonEmptyString `json:"title"`
Text valuer.UnsetOrNonEmptyString `json:"text"`
}
func (c ChannelMSTeamsConfig) Validate() error {
if c.WebhookURL == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.webhookUrl is required for an msteams channel")
}
return nil
}
func (c ChannelMSTeamsConfig) toUndefaultedReceiver(displayName string) (*Receiver, error) {
webhookURL, err := parseSecretURL(c.WebhookURL)
if err != nil {
return nil, err
}
return &Receiver{Receiver: &config.Receiver{
Name: displayName,
MSTeamsV2Configs: []*config.MSTeamsV2Config{{
NotifierConfig: config.NotifierConfig{VSendResolved: resolveSendResolved(c.SendResolved, config.DefaultMSTeamsV2Config.VSendResolved)},
WebhookURL: webhookURL,
Title: c.Title.StringValue(),
Text: c.Text.StringValue(),
}},
}}, nil
}
func newChannelMSTeamsConfigFromReceiver(name string, receiver *Receiver) (ChannelSpec, error) {
msteams := receiver.MSTeamsV2Configs[0]
sendResolved := msteams.VSendResolved
if err := rejectAnyHTTPAuth(name, msteams.HTTPConfig); err != nil {
return nil, err
}
return &ChannelMSTeamsConfig{
SendResolved: &sendResolved,
WebhookURL: formatSecretURL(msteams.WebhookURL),
Title: valuer.UnsetIfEmpty(msteams.Title),
Text: valuer.UnsetIfEmpty(msteams.Text),
}, nil
}

View File

@@ -1,71 +0,0 @@
package alertmanagertypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/prometheus/alertmanager/config"
)
type ChannelOpsgenieConfig struct {
SendResolved *bool `json:"sendResolved,omitempty"`
APIKey string `json:"apiKey" required:"true" format:"password"`
APIURL string `json:"apiUrl"`
Message valuer.UnsetOrNonEmptyString `json:"message"`
Description valuer.UnsetOrNonEmptyString `json:"description"`
Source valuer.UnsetOrNonEmptyString `json:"source"`
Details map[string]string `json:"details,omitempty"`
Priority string `json:"priority"`
}
func (c ChannelOpsgenieConfig) Validate() error {
if c.APIKey == "" {
return errors.NewInvalidInputf(ErrCodeAlertmanagerChannelInvalid, "config.spec.apiKey is required for an opsgenie channel")
}
return nil
}
func (c ChannelOpsgenieConfig) toUndefaultedReceiver(displayName string) (*Receiver, error) {
var apiURL *config.URL
if c.APIURL != "" {
parsed, err := parseUpstreamURL(c.APIURL)
if err != nil {
return nil, err
}
apiURL = parsed
}
return &Receiver{Receiver: &config.Receiver{
Name: displayName,
OpsGenieConfigs: []*config.OpsGenieConfig{{
NotifierConfig: config.NotifierConfig{VSendResolved: resolveSendResolved(c.SendResolved, config.DefaultOpsGenieConfig.VSendResolved)},
APIKey: config.Secret(c.APIKey),
APIURL: apiURL,
Message: c.Message.StringValue(),
Description: c.Description.StringValue(),
Source: c.Source.StringValue(),
Priority: c.Priority,
Details: c.Details,
}},
}}, nil
}
func newChannelOpsgenieConfigFromReceiver(name string, receiver *Receiver) (ChannelSpec, error) {
opsgenie := receiver.OpsGenieConfigs[0]
sendResolved := opsgenie.VSendResolved
if err := rejectAnyHTTPAuth(name, opsgenie.HTTPConfig); err != nil {
return nil, err
}
return &ChannelOpsgenieConfig{
SendResolved: &sendResolved,
APIKey: string(opsgenie.APIKey),
APIURL: formatUpstreamURL(opsgenie.APIURL),
Message: valuer.UnsetIfEmpty(opsgenie.Message),
Description: valuer.UnsetIfEmpty(opsgenie.Description),
Source: valuer.UnsetIfEmpty(opsgenie.Source),
Priority: opsgenie.Priority,
Details: opsgenie.Details,
}, nil
}

Some files were not shown because too many files have changed in this diff Show More