nora-lib-impl 1.0.2__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/PKG-INFO +1 -1
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/context/agent_context.py +8 -1
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/interactions/models.py +215 -15
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib_impl.egg-info/PKG-INFO +1 -1
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/README.md +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/pyproject.toml +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/setup.cfg +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/setup.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/context/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/context/context_service.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/context/models.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/interactions/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/interactions/interactions_service.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/interactions/step_progress.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/pubsub.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/tasks/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/tasks/state.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/progress/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/progress/models.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/progress/reporter.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/py.typed +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/serializers.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/tasks/__init__.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/tasks/models.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/tasks/state.py +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib_impl.egg-info/SOURCES.txt +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib_impl.egg-info/dependency_links.txt +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib_impl.egg-info/requires.txt +0 -0
- {nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib_impl.egg-info/top_level.txt +0 -0
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
|
+
from uuid import UUID
|
|
2
3
|
|
|
3
4
|
from nora_lib.impl.interactions.models import Surface
|
|
4
5
|
from pydantic import BaseModel
|
|
5
6
|
|
|
7
|
+
from nora_lib.serializers import UuidWithSerializer
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
class MessageAgentContext(BaseModel):
|
|
8
11
|
"""
|
|
9
|
-
Identifiers for the triggering
|
|
12
|
+
Identifiers for the triggering message
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
message_id: str
|
|
13
16
|
thread_id: str
|
|
14
17
|
channel_id: str
|
|
18
|
+
# Pydantic models don't actually need this, but some codebases pass model_dump() objects
|
|
19
|
+
# to other JSON serializers (e.g. FastAPI) which don't handle UUIDs and other python built-ins.
|
|
20
|
+
# So we use this special UUID wrapper for convenience.
|
|
21
|
+
actor_id: UuidWithSerializer
|
|
15
22
|
surface: Surface
|
|
16
23
|
|
|
17
24
|
|
|
@@ -5,10 +5,18 @@ Model for interactions to be sent to the interactions service.
|
|
|
5
5
|
import json
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from enum import Enum
|
|
8
|
-
from typing import Dict, List, Optional, Tuple
|
|
8
|
+
from typing import Annotated, Dict, List, Literal, Optional, Tuple, Union
|
|
9
9
|
from uuid import UUID
|
|
10
10
|
|
|
11
|
-
from pydantic import
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Discriminator,
|
|
15
|
+
Field,
|
|
16
|
+
field_serializer,
|
|
17
|
+
field_validator,
|
|
18
|
+
model_validator,
|
|
19
|
+
)
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
class Surface(str, Enum):
|
|
@@ -300,25 +308,51 @@ class VirtualThread:
|
|
|
300
308
|
class CostDetail(BaseModel):
|
|
301
309
|
"""
|
|
302
310
|
Base class to store details of cost to service a request by an agent.
|
|
303
|
-
If an agent has different cost details,
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
If an agent has different cost details (e.g. for non-llm costs), it should:
|
|
312
|
+
|
|
313
|
+
- create another class inheriting this class and add any additional fields
|
|
314
|
+
- give the class a unique detail_type
|
|
315
|
+
- add the class to CostDetailType below
|
|
316
|
+
|
|
317
|
+
See LLMCost below as an example.
|
|
318
|
+
|
|
319
|
+
To add new details about the cost of an LLM call, add to LLMCost below
|
|
320
|
+
rather than creating a new detail. A ServiceCost can have details of
|
|
321
|
+
multiple LLM calls, so if details are split into multiple instances then
|
|
322
|
+
they can't easily be connected.
|
|
323
|
+
|
|
324
|
+
Any new fields added to an existing subclass must have a default value
|
|
325
|
+
specified for backward-compatibility.
|
|
306
326
|
"""
|
|
307
327
|
|
|
308
|
-
|
|
309
|
-
pass
|
|
328
|
+
detail_type: str = "unknown"
|
|
310
329
|
|
|
330
|
+
model_config = ConfigDict(protected_namespaces=(), extra="allow")
|
|
311
331
|
|
|
312
|
-
|
|
313
|
-
|
|
332
|
+
def try_subclass_conversion(self):
|
|
333
|
+
"""For events with no detail_type, attempt to convert to an appropriate
|
|
334
|
+
subclass based on the fields. This is useful for handling legacy
|
|
335
|
+
events but should not be needed for new events that have detail_type."""
|
|
336
|
+
# Already a subclass
|
|
337
|
+
if type(self) is not CostDetail:
|
|
338
|
+
return self
|
|
314
339
|
|
|
315
|
-
|
|
316
|
-
|
|
340
|
+
d = self.dict()
|
|
341
|
+
del d["detail_type"]
|
|
342
|
+
if "token_count" in d and "model_name" in d:
|
|
343
|
+
return LLMCost(**d)
|
|
344
|
+
if "prompt_tokens" in d and "completion_tokens" in d:
|
|
345
|
+
return LLMTokenBreakdown(**d)
|
|
346
|
+
if "run_id" in d:
|
|
347
|
+
return LangChainRun(**d)
|
|
348
|
+
return self
|
|
317
349
|
|
|
318
350
|
|
|
319
351
|
class LLMTokenBreakdown(CostDetail):
|
|
320
352
|
"""Token usage breakdown"""
|
|
321
353
|
|
|
354
|
+
detail_type: Literal["llm_token_breakdown"] = "llm_token_breakdown"
|
|
355
|
+
|
|
322
356
|
prompt_tokens: int
|
|
323
357
|
completion_tokens: int
|
|
324
358
|
|
|
@@ -326,6 +360,8 @@ class LLMTokenBreakdown(CostDetail):
|
|
|
326
360
|
class LangChainRun(CostDetail):
|
|
327
361
|
"""LangChain Run"""
|
|
328
362
|
|
|
363
|
+
detail_type: Literal["langchain_run"] = "langchain_run"
|
|
364
|
+
|
|
329
365
|
# Subset of run fields which allow future lookup of run details.
|
|
330
366
|
run_id: UUID
|
|
331
367
|
run_name: Optional[str] = None
|
|
@@ -336,15 +372,51 @@ class LangChainRun(CostDetail):
|
|
|
336
372
|
# Serialize the UUIDs as strings
|
|
337
373
|
@field_serializer("run_id")
|
|
338
374
|
def serialize_id(self, run_id: UUID):
|
|
339
|
-
return str(run_id)
|
|
375
|
+
return str(run_id) if run_id is not None else None
|
|
340
376
|
|
|
341
377
|
@field_serializer("trace_id")
|
|
342
378
|
def serialize_trace_id(self, trace_id: UUID):
|
|
343
|
-
return str(trace_id)
|
|
379
|
+
return str(trace_id) if trace_id is not None else None
|
|
344
380
|
|
|
345
381
|
@field_serializer("session_id")
|
|
346
382
|
def serialize_session_id(self, session_id: UUID):
|
|
347
|
-
return str(session_id)
|
|
383
|
+
return str(session_id) if session_id is not None else None
|
|
384
|
+
|
|
385
|
+
# Validators to handle legacy "None" strings
|
|
386
|
+
@field_validator("trace_id", "session_id", mode="before")
|
|
387
|
+
@classmethod
|
|
388
|
+
def validate_optional_uuid(cls, value):
|
|
389
|
+
if value == "None":
|
|
390
|
+
return None
|
|
391
|
+
return value
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class LLMCost(CostDetail):
|
|
395
|
+
"""Details for the cost/usage of an LLM call."""
|
|
396
|
+
|
|
397
|
+
detail_type: Literal["llm_cost"] = "llm_cost"
|
|
398
|
+
|
|
399
|
+
model_name: str
|
|
400
|
+
token_count: int
|
|
401
|
+
|
|
402
|
+
token_breakdown: Optional[LLMTokenBreakdown] = None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# Note: CostDetailType is a Union of all the subclasses of CostDetail, with
|
|
406
|
+
# a discriminator for pydantic deserialization
|
|
407
|
+
CostDetailType = Union[
|
|
408
|
+
Annotated[
|
|
409
|
+
Union[
|
|
410
|
+
LLMCost,
|
|
411
|
+
LLMTokenBreakdown,
|
|
412
|
+
LangChainRun,
|
|
413
|
+
],
|
|
414
|
+
Discriminator("detail_type"),
|
|
415
|
+
],
|
|
416
|
+
# We fall back to the base class if the discriminator is not found (legacy
|
|
417
|
+
# events or custom ones written by other apps may be missing detail_type)
|
|
418
|
+
CostDetail,
|
|
419
|
+
]
|
|
348
420
|
|
|
349
421
|
|
|
350
422
|
class ServiceCost(BaseModel):
|
|
@@ -363,10 +435,32 @@ class ServiceCost(BaseModel):
|
|
|
363
435
|
description="Agent generated task_id used to track nora assigned tasks",
|
|
364
436
|
)
|
|
365
437
|
tool_call_id: Optional[str] = None
|
|
366
|
-
details:
|
|
438
|
+
details: list[CostDetailType] = Field(default_factory=list)
|
|
367
439
|
env: Optional[str] = None
|
|
368
440
|
git_sha: Optional[str] = None
|
|
369
441
|
|
|
442
|
+
def with_unified_llm_costs(self) -> "ServiceCost":
|
|
443
|
+
"""
|
|
444
|
+
Creates a new ServiceCost object with unified LLMCost details.
|
|
445
|
+
|
|
446
|
+
This method converts old-style separate LLMCost and LLMTokenBreakdown details
|
|
447
|
+
into unified LLMCost objects that include token breakdown information.
|
|
448
|
+
|
|
449
|
+
If the details are already in the unified format (no separate LLMTokenBreakdown
|
|
450
|
+
objects), the method will return a copy without modifying the details.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
A new ServiceCost object with unified LLMCost details
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
ValueError: If there is ambiguity in matching LLMCost with LLMTokenBreakdown
|
|
457
|
+
"""
|
|
458
|
+
unified_details = unify_llm_cost_details(self.details)
|
|
459
|
+
|
|
460
|
+
data = self.model_dump()
|
|
461
|
+
data["details"] = unified_details
|
|
462
|
+
return ServiceCost.model_validate(data)
|
|
463
|
+
|
|
370
464
|
|
|
371
465
|
class StepCost(BaseModel):
|
|
372
466
|
"""Wrapping service cost with event metadata so that it can be converted to an Event object."""
|
|
@@ -402,6 +496,112 @@ class StepCost(BaseModel):
|
|
|
402
496
|
)
|
|
403
497
|
|
|
404
498
|
|
|
499
|
+
def unify_llm_cost_details(details: List[CostDetailType]) -> List[CostDetailType]:
|
|
500
|
+
"""
|
|
501
|
+
Convert a list of old-style LLMCost and LLMTokenBreakdown details into a list
|
|
502
|
+
where these pairs are combined into the new unified LLMCost format.
|
|
503
|
+
|
|
504
|
+
This is useful for migrating old ServiceCost events which might have separate
|
|
505
|
+
LLMCost and LLMTokenBreakdown details for the same LLM call.
|
|
506
|
+
|
|
507
|
+
If the function encounters ambiguity (can't confidently pair LLMCost with
|
|
508
|
+
its corresponding LLMTokenBreakdown), it will raise a ValueError.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
details: List of CostDetail objects that might contain old-style
|
|
512
|
+
LLMCost and LLMTokenBreakdown details
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
A new list where matching LLMCost and LLMTokenBreakdown pairs are combined
|
|
516
|
+
into unified LLMCost objects, and other details are left unchanged.
|
|
517
|
+
|
|
518
|
+
Raises:
|
|
519
|
+
ValueError: If there is ambiguity in matching LLMCost with LLMTokenBreakdown
|
|
520
|
+
"""
|
|
521
|
+
result: List[CostDetailType] = []
|
|
522
|
+
llm_costs: List[LLMCost] = []
|
|
523
|
+
token_breakdowns: List[LLMTokenBreakdown] = []
|
|
524
|
+
other_details: List[CostDetailType] = []
|
|
525
|
+
|
|
526
|
+
if any(
|
|
527
|
+
isinstance(detail, LLMCost) and detail.token_breakdown is not None
|
|
528
|
+
for detail in details
|
|
529
|
+
) and any(isinstance(detail, LLMTokenBreakdown) for detail in details):
|
|
530
|
+
raise ValueError(
|
|
531
|
+
"Cannot mix LLMCost with token breakdowns with unified LLMCost details"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Separate details by type
|
|
535
|
+
for detail in details:
|
|
536
|
+
if isinstance(detail, LLMCost):
|
|
537
|
+
llm_costs.append(detail)
|
|
538
|
+
elif isinstance(detail, LLMTokenBreakdown):
|
|
539
|
+
token_breakdowns.append(detail)
|
|
540
|
+
else:
|
|
541
|
+
other_details.append(detail)
|
|
542
|
+
|
|
543
|
+
# If there are no token breakdowns, just return the original details
|
|
544
|
+
if not token_breakdowns:
|
|
545
|
+
return details
|
|
546
|
+
|
|
547
|
+
# If there's a mismatch in count, we can't be confident about pairing
|
|
548
|
+
if len(llm_costs) != len(token_breakdowns):
|
|
549
|
+
raise ValueError(
|
|
550
|
+
f"Cannot confidently pair LLMCost and LLMTokenBreakdown details: "
|
|
551
|
+
f"Found {len(llm_costs)} LLMCost and {len(token_breakdowns)} LLMTokenBreakdown details."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Try to pair costs with breakdowns based on token counts
|
|
555
|
+
matched_breakdowns = set()
|
|
556
|
+
|
|
557
|
+
for cost in llm_costs:
|
|
558
|
+
match_found = False
|
|
559
|
+
matching_breakdown = None
|
|
560
|
+
matched_index = -1
|
|
561
|
+
|
|
562
|
+
# Look for a token breakdown where total tokens matches the cost token count
|
|
563
|
+
for i, breakdown in enumerate(token_breakdowns):
|
|
564
|
+
if i in matched_breakdowns:
|
|
565
|
+
continue
|
|
566
|
+
|
|
567
|
+
total_tokens = breakdown.prompt_tokens + breakdown.completion_tokens
|
|
568
|
+
if total_tokens == cost.token_count:
|
|
569
|
+
if match_found:
|
|
570
|
+
# If we already found a matching breakdown, we have ambiguity
|
|
571
|
+
raise ValueError(
|
|
572
|
+
f"Ambiguity in matching LLMCost with token count {cost.token_count} "
|
|
573
|
+
f"to LLMTokenBreakdown - multiple matches found."
|
|
574
|
+
)
|
|
575
|
+
match_found = True
|
|
576
|
+
matching_breakdown = breakdown
|
|
577
|
+
matched_index = i
|
|
578
|
+
|
|
579
|
+
if match_found and matching_breakdown is not None:
|
|
580
|
+
# Create a new unified LLMCost object
|
|
581
|
+
new_cost = LLMCost(
|
|
582
|
+
model_name=cost.model_name,
|
|
583
|
+
token_count=cost.token_count,
|
|
584
|
+
token_breakdown=matching_breakdown,
|
|
585
|
+
)
|
|
586
|
+
result.append(new_cost)
|
|
587
|
+
matched_breakdowns.add(matched_index)
|
|
588
|
+
else:
|
|
589
|
+
# If no matching breakdown was found based on token count
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"Could not find matching LLMTokenBreakdown for LLMCost with token count {cost.token_count}"
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Check if all breakdowns were matched
|
|
595
|
+
if len(matched_breakdowns) != len(token_breakdowns):
|
|
596
|
+
raise ValueError("Not all LLMTokenBreakdown details were matched to an LLMCost")
|
|
597
|
+
|
|
598
|
+
# Add the other details
|
|
599
|
+
for detail in other_details:
|
|
600
|
+
result.append(detail)
|
|
601
|
+
|
|
602
|
+
return result
|
|
603
|
+
|
|
604
|
+
|
|
405
605
|
def thread_message_lookup_request(message_id: str, event_type: str) -> dict:
|
|
406
606
|
"""retrieve messages and events for the thread associated with a message"""
|
|
407
607
|
return {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nora_lib_impl-1.0.2 → nora_lib_impl-1.2.0}/src/nora_lib/impl/interactions/interactions_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|