remdb 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (187) hide show
  1. rem/__init__.py +2 -0
  2. rem/agentic/README.md +650 -0
  3. rem/agentic/__init__.py +39 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +8 -0
  6. rem/agentic/context.py +148 -0
  7. rem/agentic/context_builder.py +329 -0
  8. rem/agentic/mcp/__init__.py +0 -0
  9. rem/agentic/mcp/tool_wrapper.py +107 -0
  10. rem/agentic/otel/__init__.py +5 -0
  11. rem/agentic/otel/setup.py +151 -0
  12. rem/agentic/providers/phoenix.py +674 -0
  13. rem/agentic/providers/pydantic_ai.py +572 -0
  14. rem/agentic/query.py +117 -0
  15. rem/agentic/query_helper.py +89 -0
  16. rem/agentic/schema.py +396 -0
  17. rem/agentic/serialization.py +245 -0
  18. rem/agentic/tools/__init__.py +5 -0
  19. rem/agentic/tools/rem_tools.py +231 -0
  20. rem/api/README.md +420 -0
  21. rem/api/main.py +324 -0
  22. rem/api/mcp_router/prompts.py +182 -0
  23. rem/api/mcp_router/resources.py +536 -0
  24. rem/api/mcp_router/server.py +213 -0
  25. rem/api/mcp_router/tools.py +584 -0
  26. rem/api/routers/auth.py +229 -0
  27. rem/api/routers/chat/__init__.py +5 -0
  28. rem/api/routers/chat/completions.py +281 -0
  29. rem/api/routers/chat/json_utils.py +76 -0
  30. rem/api/routers/chat/models.py +124 -0
  31. rem/api/routers/chat/streaming.py +185 -0
  32. rem/auth/README.md +258 -0
  33. rem/auth/__init__.py +26 -0
  34. rem/auth/middleware.py +100 -0
  35. rem/auth/providers/__init__.py +13 -0
  36. rem/auth/providers/base.py +376 -0
  37. rem/auth/providers/google.py +163 -0
  38. rem/auth/providers/microsoft.py +237 -0
  39. rem/cli/README.md +455 -0
  40. rem/cli/__init__.py +8 -0
  41. rem/cli/commands/README.md +126 -0
  42. rem/cli/commands/__init__.py +3 -0
  43. rem/cli/commands/ask.py +565 -0
  44. rem/cli/commands/configure.py +423 -0
  45. rem/cli/commands/db.py +493 -0
  46. rem/cli/commands/dreaming.py +324 -0
  47. rem/cli/commands/experiments.py +1124 -0
  48. rem/cli/commands/mcp.py +66 -0
  49. rem/cli/commands/process.py +245 -0
  50. rem/cli/commands/schema.py +183 -0
  51. rem/cli/commands/serve.py +106 -0
  52. rem/cli/dreaming.py +363 -0
  53. rem/cli/main.py +88 -0
  54. rem/config.py +237 -0
  55. rem/mcp_server.py +41 -0
  56. rem/models/core/__init__.py +49 -0
  57. rem/models/core/core_model.py +64 -0
  58. rem/models/core/engram.py +333 -0
  59. rem/models/core/experiment.py +628 -0
  60. rem/models/core/inline_edge.py +132 -0
  61. rem/models/core/rem_query.py +243 -0
  62. rem/models/entities/__init__.py +43 -0
  63. rem/models/entities/file.py +57 -0
  64. rem/models/entities/image_resource.py +88 -0
  65. rem/models/entities/message.py +35 -0
  66. rem/models/entities/moment.py +123 -0
  67. rem/models/entities/ontology.py +191 -0
  68. rem/models/entities/ontology_config.py +131 -0
  69. rem/models/entities/resource.py +95 -0
  70. rem/models/entities/schema.py +87 -0
  71. rem/models/entities/user.py +85 -0
  72. rem/py.typed +0 -0
  73. rem/schemas/README.md +507 -0
  74. rem/schemas/__init__.py +6 -0
  75. rem/schemas/agents/README.md +92 -0
  76. rem/schemas/agents/core/moment-builder.yaml +178 -0
  77. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  78. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  79. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  80. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  81. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  82. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  83. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  84. rem/schemas/agents/examples/hello-world.yaml +37 -0
  85. rem/schemas/agents/examples/query.yaml +54 -0
  86. rem/schemas/agents/examples/simple.yaml +21 -0
  87. rem/schemas/agents/examples/test.yaml +29 -0
  88. rem/schemas/agents/rem.yaml +128 -0
  89. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  90. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  91. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  92. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  93. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  94. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  95. rem/services/__init__.py +16 -0
  96. rem/services/audio/INTEGRATION.md +308 -0
  97. rem/services/audio/README.md +376 -0
  98. rem/services/audio/__init__.py +15 -0
  99. rem/services/audio/chunker.py +354 -0
  100. rem/services/audio/transcriber.py +259 -0
  101. rem/services/content/README.md +1269 -0
  102. rem/services/content/__init__.py +5 -0
  103. rem/services/content/providers.py +806 -0
  104. rem/services/content/service.py +657 -0
  105. rem/services/dreaming/README.md +230 -0
  106. rem/services/dreaming/__init__.py +53 -0
  107. rem/services/dreaming/affinity_service.py +336 -0
  108. rem/services/dreaming/moment_service.py +264 -0
  109. rem/services/dreaming/ontology_service.py +54 -0
  110. rem/services/dreaming/user_model_service.py +297 -0
  111. rem/services/dreaming/utils.py +39 -0
  112. rem/services/embeddings/__init__.py +11 -0
  113. rem/services/embeddings/api.py +120 -0
  114. rem/services/embeddings/worker.py +421 -0
  115. rem/services/fs/README.md +662 -0
  116. rem/services/fs/__init__.py +62 -0
  117. rem/services/fs/examples.py +206 -0
  118. rem/services/fs/examples_paths.py +204 -0
  119. rem/services/fs/git_provider.py +935 -0
  120. rem/services/fs/local_provider.py +760 -0
  121. rem/services/fs/parsing-hooks-examples.md +172 -0
  122. rem/services/fs/paths.py +276 -0
  123. rem/services/fs/provider.py +460 -0
  124. rem/services/fs/s3_provider.py +1042 -0
  125. rem/services/fs/service.py +186 -0
  126. rem/services/git/README.md +1075 -0
  127. rem/services/git/__init__.py +17 -0
  128. rem/services/git/service.py +469 -0
  129. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  130. rem/services/phoenix/README.md +453 -0
  131. rem/services/phoenix/__init__.py +46 -0
  132. rem/services/phoenix/client.py +686 -0
  133. rem/services/phoenix/config.py +88 -0
  134. rem/services/phoenix/prompt_labels.py +477 -0
  135. rem/services/postgres/README.md +575 -0
  136. rem/services/postgres/__init__.py +23 -0
  137. rem/services/postgres/migration_service.py +427 -0
  138. rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
  139. rem/services/postgres/register_type.py +352 -0
  140. rem/services/postgres/repository.py +337 -0
  141. rem/services/postgres/schema_generator.py +379 -0
  142. rem/services/postgres/service.py +802 -0
  143. rem/services/postgres/sql_builder.py +354 -0
  144. rem/services/rem/README.md +304 -0
  145. rem/services/rem/__init__.py +23 -0
  146. rem/services/rem/exceptions.py +71 -0
  147. rem/services/rem/executor.py +293 -0
  148. rem/services/rem/parser.py +145 -0
  149. rem/services/rem/queries.py +196 -0
  150. rem/services/rem/query.py +371 -0
  151. rem/services/rem/service.py +527 -0
  152. rem/services/session/README.md +374 -0
  153. rem/services/session/__init__.py +6 -0
  154. rem/services/session/compression.py +360 -0
  155. rem/services/session/reload.py +77 -0
  156. rem/settings.py +1235 -0
  157. rem/sql/002_install_models.sql +1068 -0
  158. rem/sql/background_indexes.sql +42 -0
  159. rem/sql/install_models.sql +1038 -0
  160. rem/sql/migrations/001_install.sql +503 -0
  161. rem/sql/migrations/002_install_models.sql +1202 -0
  162. rem/utils/AGENTIC_CHUNKING.md +597 -0
  163. rem/utils/README.md +583 -0
  164. rem/utils/__init__.py +43 -0
  165. rem/utils/agentic_chunking.py +622 -0
  166. rem/utils/batch_ops.py +343 -0
  167. rem/utils/chunking.py +108 -0
  168. rem/utils/clip_embeddings.py +276 -0
  169. rem/utils/dict_utils.py +98 -0
  170. rem/utils/embeddings.py +423 -0
  171. rem/utils/examples/embeddings_example.py +305 -0
  172. rem/utils/examples/sql_types_example.py +202 -0
  173. rem/utils/markdown.py +16 -0
  174. rem/utils/model_helpers.py +236 -0
  175. rem/utils/schema_loader.py +229 -0
  176. rem/utils/sql_types.py +348 -0
  177. rem/utils/user_id.py +81 -0
  178. rem/utils/vision.py +330 -0
  179. rem/workers/README.md +506 -0
  180. rem/workers/__init__.py +5 -0
  181. rem/workers/dreaming.py +502 -0
  182. rem/workers/engram_processor.py +312 -0
  183. rem/workers/sqs_file_processor.py +193 -0
  184. remdb-0.2.6.dist-info/METADATA +1191 -0
  185. remdb-0.2.6.dist-info/RECORD +187 -0
  186. remdb-0.2.6.dist-info/WHEEL +4 -0
  187. remdb-0.2.6.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,88 @@
1
+ """Phoenix configuration for REM.
2
+
3
+ Loads connection settings from environment variables with sensible defaults.
4
+ """
5
+
6
+ import os
7
+ from loguru import logger
8
+
9
+
10
+ class PhoenixConfig:
11
+ """Phoenix connection configuration.
12
+
13
+ Environment Variables:
14
+ - PHOENIX_BASE_URL: Phoenix server URL (default: http://localhost:6006)
15
+ - PHOENIX_API_KEY: API key for authentication (required for cluster Phoenix)
16
+
17
+ Deployment Patterns:
18
+ --------------------
19
+
20
+ **Production (Cluster Phoenix)** - RECOMMENDED:
21
+ REM typically runs on Kubernetes alongside Phoenix in the observability namespace.
22
+ Experiments run directly on the cluster where Phoenix is deployed:
23
+
24
+ # 1. Deploy experiment as K8s Job or run from rem-api pod
25
+ kubectl exec -it deployment/rem-api -- rem experiments run my-experiment
26
+
27
+ # 2. Phoenix accessible via service DNS
28
+ export PHOENIX_BASE_URL=http://phoenix-svc.observability.svc.cluster.local:6006
29
+ export PHOENIX_API_KEY=<your-key>
30
+
31
+ **Development (Port-Forward)** - For local testing:
32
+ Port-forward Phoenix from cluster to local machine:
33
+
34
+ # 1. Port-forward Phoenix service
35
+ kubectl port-forward -n observability svc/phoenix-svc 6006:6006
36
+
37
+ # 2. Set API key
38
+ export PHOENIX_API_KEY=<your-key>
39
+
40
+ # 3. Run experiments locally
41
+ rem experiments run my-experiment
42
+ # Connects to localhost:6006 → cluster Phoenix
43
+
44
+ **Local Development (Local Phoenix)** - For offline work:
45
+ Run Phoenix locally without cluster connection:
46
+
47
+ # 1. Start local Phoenix
48
+ python -m phoenix.server.main serve
49
+
50
+ # 2. Run experiments (no API key needed)
51
+ rem experiments run my-experiment
52
+ # Connects to localhost:6006 → local Phoenix
53
+
54
+ Override Defaults:
55
+ ------------------
56
+ Commands respect PHOENIX_BASE_URL and PHOENIX_API_KEY environment variables.
57
+ Default is localhost:6006 for local development compatibility.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ base_url: str | None = None,
63
+ api_key: str | None = None,
64
+ ):
65
+ """Initialize Phoenix configuration.
66
+
67
+ Args:
68
+ base_url: Phoenix server URL (overrides env var)
69
+ api_key: API key for authentication (overrides env var)
70
+ """
71
+ self.base_url = base_url or os.getenv(
72
+ "PHOENIX_BASE_URL", "http://localhost:6006"
73
+ )
74
+ self.api_key = api_key or os.getenv("PHOENIX_API_KEY")
75
+
76
+ logger.debug(f"Phoenix config: base_url={self.base_url}, api_key={'***' if self.api_key else 'None'}")
77
+
78
+ @classmethod
79
+ def from_settings(cls) -> "PhoenixConfig":
80
+ """Load Phoenix configuration from REM settings.
81
+
82
+ Returns:
83
+ PhoenixConfig with values from settings
84
+
85
+ Note: Currently loads from env vars. Could be extended to use
86
+ rem.settings.Settings if Phoenix settings are added there.
87
+ """
88
+ return cls()
@@ -0,0 +1,477 @@
1
+ """Phoenix prompt label management via GraphQL.
2
+
3
+ This module provides utilities for creating and assigning labels to prompts
4
+ in Phoenix using the GraphQL API.
5
+
6
+ Based on carrier implementation with adaptations for REM:
7
+ - Standard REM labels: rem, golden-set, agent-results, evaluator
8
+ - GraphQL operations: create, list, assign labels
9
+ - Color-coded label system
10
+ """
11
+
12
+ import httpx
13
+ from typing import Optional
14
+ from loguru import logger
15
+
16
+
17
+ class PromptLabelQueries:
18
+ """GraphQL queries for prompt and dataset label operations."""
19
+
20
+ CREATE_PROMPT_LABEL = """
21
+ mutation CreateLabel($input: CreatePromptLabelInput!) {
22
+ createPromptLabel(input: $input) {
23
+ promptLabels {
24
+ id
25
+ name
26
+ color
27
+ description
28
+ }
29
+ }
30
+ }
31
+ """
32
+
33
+ CREATE_DATASET_LABEL = """
34
+ mutation CreateDatasetLabel($input: CreateDatasetLabelInput!) {
35
+ createDatasetLabel(input: $input) {
36
+ datasetLabel {
37
+ id
38
+ name
39
+ color
40
+ description
41
+ }
42
+ }
43
+ }
44
+ """
45
+
46
+ LIST_PROMPT_LABELS = """
47
+ query {
48
+ promptLabels {
49
+ edges {
50
+ node {
51
+ id
52
+ name
53
+ color
54
+ description
55
+ }
56
+ }
57
+ }
58
+ }
59
+ """
60
+
61
+ LIST_DATASET_LABELS = """
62
+ query {
63
+ datasetLabels {
64
+ edges {
65
+ node {
66
+ id
67
+ name
68
+ color
69
+ description
70
+ }
71
+ }
72
+ }
73
+ }
74
+ """
75
+
76
+ GET_DATASET_BY_NAME = """
77
+ query GetDataset($name: String!) {
78
+ datasets(first: 1, filterBy: {name: {equals: $name}}) {
79
+ edges {
80
+ node {
81
+ id
82
+ name
83
+ }
84
+ }
85
+ }
86
+ }
87
+ """
88
+
89
+ ASSIGN_PROMPT_LABELS = """
90
+ mutation SetLabels($promptId: ID!, $promptLabelIds: [ID!]!) {
91
+ setPromptLabels(input: {
92
+ promptId: $promptId
93
+ promptLabelIds: $promptLabelIds
94
+ }) {
95
+ query {
96
+ node(id: $promptId) {
97
+ __typename
98
+ ... on Prompt {
99
+ id
100
+ name
101
+ labels {
102
+ id
103
+ name
104
+ color
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ """
112
+
113
+ ASSIGN_DATASET_LABELS = """
114
+ mutation SetDatasetLabels($datasetId: ID!, $datasetLabelIds: [ID!]!) {
115
+ setDatasetLabels(input: {
116
+ datasetId: $datasetId
117
+ datasetLabelIds: $datasetLabelIds
118
+ }) {
119
+ dataset {
120
+ id
121
+ labels {
122
+ id
123
+ name
124
+ color
125
+ }
126
+ }
127
+ }
128
+ }
129
+ """
130
+
131
+
132
+ class GraphQLClient:
133
+ """Simple GraphQL client for Phoenix API."""
134
+
135
+ def __init__(self, base_url: str, api_key: Optional[str] = None):
136
+ """Initialize GraphQL client.
137
+
138
+ Args:
139
+ base_url: Phoenix base URL
140
+ api_key: Optional API key for auth
141
+ """
142
+ self.base_url = base_url
143
+ self.api_key = api_key
144
+
145
+ def execute(self, query: str, variables: Optional[dict] = None) -> dict:
146
+ """Execute GraphQL query.
147
+
148
+ Args:
149
+ query: GraphQL query/mutation string
150
+ variables: Optional variables dict
151
+
152
+ Returns:
153
+ Response data dict
154
+
155
+ Raises:
156
+ httpx.HTTPStatusError: On HTTP errors
157
+ """
158
+ headers = {"Content-Type": "application/json"}
159
+ if self.api_key:
160
+ headers["Authorization"] = f"Bearer {self.api_key}"
161
+
162
+ response = httpx.post(
163
+ f"{self.base_url}/graphql",
164
+ headers=headers,
165
+ json={"query": query, "variables": variables},
166
+ timeout=10,
167
+ )
168
+ response.raise_for_status()
169
+
170
+ result = response.json()
171
+
172
+ # Log errors but don't raise
173
+ if result.get("errors"):
174
+ error_msgs = [e.get("message", "Unknown error") for e in result["errors"]]
175
+ logger.debug(f"GraphQL errors: {error_msgs}")
176
+
177
+ return result
178
+
179
+
180
+ class PhoenixPromptLabels:
181
+ """Helper for managing Phoenix prompt labels via GraphQL."""
182
+
183
+ def __init__(self, base_url: str = "http://localhost:6006", api_key: Optional[str] = None):
184
+ """Initialize label manager.
185
+
186
+ Args:
187
+ base_url: Phoenix base URL
188
+ api_key: Phoenix API key (optional, for auth)
189
+ """
190
+ self.client = GraphQLClient(base_url, api_key)
191
+ self._label_cache: Optional[dict[str, str]] = None
192
+
193
+ def create_prompt_label(
194
+ self,
195
+ name: str,
196
+ color: str,
197
+ description: str = "",
198
+ ) -> Optional[str]:
199
+ """Create a new prompt label.
200
+
201
+ Args:
202
+ name: Label name (lowercase, hyphens, underscores)
203
+ color: Color in rgba(r, g, b, a) format
204
+ description: Optional description
205
+
206
+ Returns:
207
+ Label ID if created, None if already exists
208
+
209
+ Raises:
210
+ httpx.HTTPStatusError: On HTTP errors
211
+ """
212
+ variables = {"input": {"name": name, "color": color, "description": description}}
213
+ result = self.client.execute(PromptLabelQueries.CREATE_PROMPT_LABEL, variables)
214
+
215
+ # Check if already exists
216
+ if result.get("errors"):
217
+ for error in result["errors"]:
218
+ if "already exists" in error.get("message", ""):
219
+ logger.debug(f"Prompt label '{name}' already exists")
220
+ return None
221
+
222
+ # Extract label ID from response
223
+ if result.get("data") and result["data"].get("createPromptLabel"):
224
+ labels = result["data"]["createPromptLabel"]["promptLabels"]
225
+ if labels:
226
+ label_id = labels[0]["id"]
227
+ logger.info(f"Created prompt label '{name}' ({label_id})")
228
+ # Invalidate cache
229
+ self._label_cache = None
230
+ return label_id
231
+
232
+ return None
233
+
234
+ def create_dataset_label(
235
+ self,
236
+ name: str,
237
+ color: str,
238
+ description: str = "",
239
+ ) -> Optional[str]:
240
+ """Create a new dataset label.
241
+
242
+ Args:
243
+ name: Label name
244
+ color: Color in rgba(r, g, b, a) format
245
+ description: Optional description
246
+
247
+ Returns:
248
+ Label ID if created, None if already exists
249
+
250
+ Raises:
251
+ httpx.HTTPStatusError: On HTTP errors
252
+ """
253
+ variables = {"input": {"name": name, "color": color, "description": description}}
254
+ result = self.client.execute(PromptLabelQueries.CREATE_DATASET_LABEL, variables)
255
+
256
+ # Check if already exists
257
+ if result.get("errors"):
258
+ for error in result["errors"]:
259
+ if "already exists" in error.get("message", ""):
260
+ logger.debug(f"Dataset label '{name}' already exists")
261
+ return None
262
+
263
+ # Extract label ID from response
264
+ if result.get("data") and result["data"].get("createDatasetLabel"):
265
+ label = result["data"]["createDatasetLabel"]["datasetLabel"]
266
+ label_id = label["id"]
267
+ logger.info(f"Created dataset label '{name}' ({label_id})")
268
+ return label_id
269
+
270
+ return None
271
+
272
+ def list_prompt_labels(self, force_refresh: bool = False) -> dict[str, str]:
273
+ """List all available prompt labels.
274
+
275
+ Args:
276
+ force_refresh: Force refresh cache
277
+
278
+ Returns:
279
+ Dict mapping label name to label ID
280
+ """
281
+ if self._label_cache and not force_refresh:
282
+ return self._label_cache
283
+
284
+ result = self.client.execute(PromptLabelQueries.LIST_PROMPT_LABELS)
285
+ labels = {}
286
+
287
+ if result.get("data", {}).get("promptLabels"):
288
+ for edge in result["data"]["promptLabels"]["edges"]:
289
+ label = edge["node"]
290
+ labels[label["name"]] = label["id"]
291
+
292
+ self._label_cache = labels
293
+ return labels
294
+
295
+ def list_dataset_labels(self) -> dict[str, str]:
296
+ """List all available dataset labels.
297
+
298
+ Returns:
299
+ Dict mapping label name to label ID
300
+ """
301
+ result = self.client.execute(PromptLabelQueries.LIST_DATASET_LABELS)
302
+ labels = {}
303
+
304
+ if result.get("data", {}).get("datasetLabels"):
305
+ for edge in result["data"]["datasetLabels"]["edges"]:
306
+ label = edge["node"]
307
+ labels[label["name"]] = label["id"]
308
+
309
+ return labels
310
+
311
+ def ensure_prompt_labels(self, labels: list[tuple[str, str, str]]) -> dict[str, str]:
312
+ """Ensure prompt labels exist, creating them if needed.
313
+
314
+ Args:
315
+ labels: List of (name, color, description) tuples
316
+
317
+ Returns:
318
+ Dict mapping label name to label ID
319
+ """
320
+ existing = self.list_prompt_labels(force_refresh=True)
321
+
322
+ for name, color, description in labels:
323
+ if name not in existing:
324
+ label_id = self.create_prompt_label(name, color, description)
325
+ if label_id:
326
+ existing[name] = label_id
327
+
328
+ return existing
329
+
330
+ def ensure_dataset_labels(self, labels: list[tuple[str, str, str]]) -> dict[str, str]:
331
+ """Ensure dataset labels exist, creating them if needed.
332
+
333
+ Args:
334
+ labels: List of (name, color, description) tuples
335
+
336
+ Returns:
337
+ Dict mapping label name to label ID
338
+ """
339
+ existing = self.list_dataset_labels()
340
+
341
+ for name, color, description in labels:
342
+ if name not in existing:
343
+ label_id = self.create_dataset_label(name, color, description)
344
+ if label_id:
345
+ existing[name] = label_id
346
+
347
+ return existing
348
+
349
+ def assign_prompt_labels(
350
+ self,
351
+ prompt_id: str,
352
+ label_names: list[str],
353
+ ) -> list[dict]:
354
+ """Assign labels to a prompt.
355
+
356
+ Args:
357
+ prompt_id: Prompt ID (from prompts GraphQL query)
358
+ label_names: List of label names to assign
359
+
360
+ Returns:
361
+ List of assigned labels with id, name, color
362
+
363
+ Raises:
364
+ ValueError: If label not found
365
+ httpx.HTTPStatusError: On HTTP errors
366
+ """
367
+ available_labels = self.list_prompt_labels()
368
+
369
+ # Convert label names to IDs
370
+ label_ids = []
371
+ for name in label_names:
372
+ if name not in available_labels:
373
+ raise ValueError(
374
+ f"Prompt label '{name}' not found. Available: {list(available_labels.keys())}"
375
+ )
376
+ label_ids.append(available_labels[name])
377
+
378
+ variables = {"promptId": prompt_id, "promptLabelIds": label_ids}
379
+ result = self.client.execute(PromptLabelQueries.ASSIGN_PROMPT_LABELS, variables)
380
+
381
+ if result.get("data") and result["data"].get("setPromptLabels"):
382
+ node = result["data"]["setPromptLabels"]["query"]["node"]
383
+ labels = node.get("labels", [])
384
+ logger.info(f"Assigned {len(label_names)} labels to prompt {node['name']}")
385
+ return labels
386
+
387
+ return []
388
+
389
+ def get_dataset_id(self, dataset_name: str) -> str | None:
390
+ """Get dataset ID by name.
391
+
392
+ Args:
393
+ dataset_name: Dataset name
394
+
395
+ Returns:
396
+ Dataset ID if found, None otherwise
397
+ """
398
+ variables = {"name": dataset_name}
399
+ result = self.client.execute(PromptLabelQueries.GET_DATASET_BY_NAME, variables)
400
+
401
+ if result.get("data") and result["data"].get("datasets"):
402
+ edges = result["data"]["datasets"]["edges"]
403
+ if edges:
404
+ return edges[0]["node"]["id"]
405
+
406
+ return None
407
+
408
+ def assign_dataset_labels(
409
+ self,
410
+ dataset_id: str,
411
+ label_names: list[str],
412
+ ) -> bool:
413
+ """Assign labels to a dataset.
414
+
415
+ Args:
416
+ dataset_id: Dataset ID (from datasets GraphQL query)
417
+ label_names: List of label names to assign
418
+
419
+ Returns:
420
+ True if successful
421
+
422
+ Raises:
423
+ ValueError: If label not found
424
+ httpx.HTTPStatusError: On HTTP errors
425
+ """
426
+ available_labels = self.list_dataset_labels()
427
+
428
+ # Convert label names to IDs
429
+ label_ids = []
430
+ for name in label_names:
431
+ if name not in available_labels:
432
+ raise ValueError(
433
+ f"Dataset label '{name}' not found. Available: {list(available_labels.keys())}"
434
+ )
435
+ label_ids.append(available_labels[name])
436
+
437
+ variables = {"datasetId": dataset_id, "datasetLabelIds": label_ids}
438
+ result = self.client.execute(PromptLabelQueries.ASSIGN_DATASET_LABELS, variables)
439
+
440
+ if result.get("data") and result["data"].get("setDatasetLabels"):
441
+ dataset = result["data"]["setDatasetLabels"]["dataset"]
442
+ labels = dataset.get("labels", [])
443
+ logger.info(f"Assigned {len(labels)} labels to dataset {dataset_id}")
444
+ return True
445
+
446
+ return False
447
+
448
+
449
+ # Standard REM labels (shared by prompts and datasets)
450
+ # Phoenix expects hex colors (e.g., #3b82f6), not rgba format
451
+ REM_LABELS = [
452
+ ("REM", "#3b82f6", "All REM integration artifacts (auto-added)"),
453
+ ("Agent", "#10b981", "Agent prompts (auto-added for agents)"),
454
+ ("Evaluator", "#8b5cf6", "Evaluator prompts (auto-added for evaluators)"),
455
+ ("Ground Truth", "#22c55e", "Ground truth/golden set datasets (auto-added)"),
456
+ ("Test", "#f59e0b", "Test and integration artifacts"),
457
+ ("HelloWorld", "#ec4899", "Hello world test examples"),
458
+ ]
459
+
460
+
461
+ def setup_rem_labels(
462
+ base_url: str = "http://localhost:6006",
463
+ api_key: Optional[str] = None,
464
+ ) -> PhoenixPromptLabels:
465
+ """Setup standard REM prompt and dataset labels.
466
+
467
+ Args:
468
+ base_url: Phoenix base URL
469
+ api_key: Phoenix API key
470
+
471
+ Returns:
472
+ Configured PhoenixPromptLabels instance
473
+ """
474
+ helper = PhoenixPromptLabels(base_url=base_url, api_key=api_key)
475
+ helper.ensure_prompt_labels(REM_LABELS)
476
+ helper.ensure_dataset_labels(REM_LABELS)
477
+ return helper