by-framework-langgraph 0.0.3.dev2__py3-none-any.whl → 0.0.3.dev4__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.
- by_framework_langgraph/adapter.py +71 -4
- {by_framework_langgraph-0.0.3.dev2.dist-info → by_framework_langgraph-0.0.3.dev4.dist-info}/METADATA +45 -2
- {by_framework_langgraph-0.0.3.dev2.dist-info → by_framework_langgraph-0.0.3.dev4.dist-info}/RECORD +4 -4
- {by_framework_langgraph-0.0.3.dev2.dist-info → by_framework_langgraph-0.0.3.dev4.dist-info}/WHEEL +1 -1
|
@@ -18,7 +18,7 @@ from by_framework.core.protocol.commands import ResumeCommand
|
|
|
18
18
|
from by_framework.core.protocol.events import StreamChunkEvent
|
|
19
19
|
from by_framework.trace.span_recorder import str_to_uint64, str_to_uint128
|
|
20
20
|
from langchain_core.callbacks import BaseCallbackHandler
|
|
21
|
-
from langchain_core.messages import HumanMessage
|
|
21
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
22
22
|
from langgraph.types import Command
|
|
23
23
|
|
|
24
24
|
from ._utils import extract_content_text, extract_resume_data
|
|
@@ -32,6 +32,26 @@ if TYPE_CHECKING:
|
|
|
32
32
|
LANGFUSE_OBSERVATION_ATTR = "_langfuse_observation"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _last_ai_message_text(messages: list[Any]) -> str:
|
|
36
|
+
"""Find the last AIMessage's text content — not just messages[-1].
|
|
37
|
+
|
|
38
|
+
A correctly-terminating ReAct graph routes back to the agent node after
|
|
39
|
+
every tool call, so the state's last message should already be an
|
|
40
|
+
AIMessage. But a misconfigured graph (e.g. a routing function that
|
|
41
|
+
returns the string "end" instead of the END constant) can terminate
|
|
42
|
+
right after a tool call instead, leaving a ToolMessage as messages[-1].
|
|
43
|
+
Blindly using messages[-1].content would then surface a tool's raw
|
|
44
|
+
result as the "final answer" instead of the model's actual reply, so
|
|
45
|
+
this scans backwards for the last real AIMessage rather than trusting
|
|
46
|
+
positional order.
|
|
47
|
+
"""
|
|
48
|
+
for msg in reversed(messages):
|
|
49
|
+
if isinstance(msg, AIMessage):
|
|
50
|
+
content = msg.content
|
|
51
|
+
return content if isinstance(content, str) else str(content)
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
|
|
35
55
|
class _TokenAccumulatingCallbackHandler(BaseCallbackHandler):
|
|
36
56
|
"""LangChain callback handler that accumulates LLM token usage into AgentContext.
|
|
37
57
|
|
|
@@ -445,12 +465,38 @@ class LangGraphAdapter:
|
|
|
445
465
|
)
|
|
446
466
|
return {"status": AgentState.QUEUED.value}
|
|
447
467
|
|
|
468
|
+
# The checkpointed graph state's last message — not the streamed
|
|
469
|
+
# text — is the authoritative final answer, and takes priority
|
|
470
|
+
# whenever it's available (not just when full_response is empty).
|
|
471
|
+
# on_chat_model_stream isn't guaranteed to fire for every LLM call a
|
|
472
|
+
# graph makes: a model that narrates before calling a tool (e.g.
|
|
473
|
+
# "好的,我来帮你计算这个表达式!" + a tool_calls delta) can populate
|
|
474
|
+
# full_response with that preamble via a round that DID stream,
|
|
475
|
+
# while the actual final answer's round — after the tool
|
|
476
|
+
# executes — doesn't stream at all. full_response would then be
|
|
477
|
+
# non-empty but wrong (just the preamble), so an empty-only check
|
|
478
|
+
# can't catch it; graph state's messages[-1] is what the graph
|
|
479
|
+
# actually decided the final answer is, regardless of what
|
|
480
|
+
# streaming happened to capture.
|
|
481
|
+
state_text = self._extract_final_text_from_state()
|
|
482
|
+
if state_text:
|
|
483
|
+
full_response = state_text
|
|
484
|
+
|
|
448
485
|
# Emit final answer if using custom output handler
|
|
449
486
|
if self._output_handler and full_response:
|
|
450
487
|
await self._output_handler(self._context, full_response)
|
|
451
488
|
|
|
452
489
|
return full_response
|
|
453
490
|
|
|
491
|
+
def _extract_final_text_from_state(self) -> str:
|
|
492
|
+
"""Read the last AIMessage's text straight from the checkpointed
|
|
493
|
+
graph state — see the fallback's call site for why this exists."""
|
|
494
|
+
try:
|
|
495
|
+
snapshot = self._graph.get_state(self._state_config)
|
|
496
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
497
|
+
return ""
|
|
498
|
+
return _last_ai_message_text((snapshot.values or {}).get("messages", []))
|
|
499
|
+
|
|
454
500
|
async def _process_result(self, result: dict) -> Any:
|
|
455
501
|
"""Analyze graph result and determine suspended vs completed."""
|
|
456
502
|
if self._is_graph_suspended():
|
|
@@ -460,13 +506,12 @@ class LangGraphAdapter:
|
|
|
460
506
|
)
|
|
461
507
|
return {"status": AgentState.QUEUED.value}
|
|
462
508
|
|
|
463
|
-
# Extract final answer from last
|
|
509
|
+
# Extract final answer from the last AIMessage
|
|
464
510
|
messages = result.get("messages", [])
|
|
465
511
|
if not messages:
|
|
466
512
|
return result
|
|
467
513
|
|
|
468
|
-
|
|
469
|
-
answer = last_msg.content if hasattr(last_msg, "content") else str(last_msg)
|
|
514
|
+
answer = _last_ai_message_text(messages)
|
|
470
515
|
|
|
471
516
|
# Emit output
|
|
472
517
|
if self._output_handler:
|
|
@@ -525,6 +570,7 @@ class LangGraphAdapter:
|
|
|
525
570
|
self._context, "parent_message_id", ""
|
|
526
571
|
),
|
|
527
572
|
"by_framework_agent_id": getattr(self._context, "current_agent_id", ""),
|
|
573
|
+
"worker_id": getattr(self._context, "worker_id", ""),
|
|
528
574
|
"langgraph_thread_id": self._thread_id,
|
|
529
575
|
}
|
|
530
576
|
return {
|
|
@@ -538,10 +584,31 @@ class LangGraphAdapter:
|
|
|
538
584
|
|
|
539
585
|
with (
|
|
540
586
|
self._phoenix_context_manager(),
|
|
587
|
+
self._langfuse_attribute_propagation_context_manager(),
|
|
541
588
|
self._langfuse_callback_manager(callbacks),
|
|
542
589
|
):
|
|
543
590
|
yield callbacks
|
|
544
591
|
|
|
592
|
+
@contextmanager
|
|
593
|
+
def _langfuse_attribute_propagation_context_manager(self) -> Iterator[None]:
|
|
594
|
+
"""Propagate stable framework metadata to Langfuse child observations."""
|
|
595
|
+
worker_id = str(getattr(self._context, "worker_id", "") or "")
|
|
596
|
+
if not worker_id:
|
|
597
|
+
yield
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
propagate_attributes = getattr(
|
|
602
|
+
import_module("langfuse"),
|
|
603
|
+
"propagate_attributes",
|
|
604
|
+
)
|
|
605
|
+
except (ImportError, AttributeError):
|
|
606
|
+
yield
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
with propagate_attributes(metadata={"worker_id": worker_id}):
|
|
610
|
+
yield
|
|
611
|
+
|
|
545
612
|
@contextmanager
|
|
546
613
|
def _phoenix_context_manager(self) -> Iterator[None]:
|
|
547
614
|
"""Prepare OpenTelemetry context for Phoenix tracing."""
|
{by_framework_langgraph-0.0.3.dev2.dist-info → by_framework_langgraph-0.0.3.dev4.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: by-framework-langgraph
|
|
3
|
-
Version: 0.0.3.
|
|
3
|
+
Version: 0.0.3.dev4
|
|
4
4
|
Summary: LangGraph integration for by-framework
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: by-framework>=0.2.1
|
|
@@ -67,3 +67,46 @@ class OrchestratorWorker(LangGraphWorker):
|
|
|
67
67
|
llm = ChatOpenAI(model="gpt-4o").bind_tools([poet, ask])
|
|
68
68
|
# ... build and return compiled graph
|
|
69
69
|
```
|
|
70
|
+
|
|
71
|
+
## Common Pitfalls
|
|
72
|
+
|
|
73
|
+
Both of these fail **silently** — no exception, just wrong or empty results — so they're easy to lose time to.
|
|
74
|
+
|
|
75
|
+
### `add_messages` must be imported as a function, not written as a string
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from langgraph.graph.message import add_messages
|
|
79
|
+
|
|
80
|
+
# Correct — the reducer actually runs; messages accumulate across nodes.
|
|
81
|
+
messages: Annotated[list, add_messages]
|
|
82
|
+
|
|
83
|
+
# Wrong — looks plausible (some older LangGraph docs used this form), but
|
|
84
|
+
# the reducer silently does nothing: each node only sees its own output,
|
|
85
|
+
# not the accumulated history. Your agent node ends up calling the model
|
|
86
|
+
# with just the latest ToolMessage and no prior context, and typically
|
|
87
|
+
# returns an empty or confused reply.
|
|
88
|
+
messages: Annotated[list, "add_messages"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### A routing function must return the `END` constant, not the string `"end"`
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from langgraph.graph import END
|
|
95
|
+
|
|
96
|
+
# Correct — the graph terminates properly; the agent node's final
|
|
97
|
+
# AIMessage is saved to state before the graph stops.
|
|
98
|
+
def should_continue(state):
|
|
99
|
+
...
|
|
100
|
+
return END
|
|
101
|
+
|
|
102
|
+
# Wrong — "end" looks like it should work as a node name, but LangGraph
|
|
103
|
+
# only recognizes the END constant (whose actual value is "__end__").
|
|
104
|
+
# LangGraph logs "wrote to unknown channel branch:to:end, ignoring it."
|
|
105
|
+
# and the graph doesn't route there — the agent's last turn is dropped,
|
|
106
|
+
# and `state["messages"][-1]` after execution can end up being something
|
|
107
|
+
# other than the model's real final answer (e.g. a stale ToolMessage from
|
|
108
|
+
# an earlier step), not the reply you expected.
|
|
109
|
+
def should_continue(state):
|
|
110
|
+
...
|
|
111
|
+
return "end"
|
|
112
|
+
```
|
{by_framework_langgraph-0.0.3.dev2.dist-info → by_framework_langgraph-0.0.3.dev4.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
by_framework_langgraph/__init__.py,sha256=x08wIaK4VRnUnUD_mbBxVgI2Zf7CvwAVbr3syP2NQps,1046
|
|
2
2
|
by_framework_langgraph/_utils.py,sha256=A8eQ19YqyBiMKcPIlzmwggN_mK5iQvNJPTaTmsywWsw,1605
|
|
3
|
-
by_framework_langgraph/adapter.py,sha256=
|
|
3
|
+
by_framework_langgraph/adapter.py,sha256=3ZYzpcbWAo_-EmQ9QzxYE7e_dxnfThiHk0wDqsF5OiA,29859
|
|
4
4
|
by_framework_langgraph/tools.py,sha256=l7LurtKKXA1Z4fyj15ffFWyWpt8drNCJ-mkGmlS7EVo,5561
|
|
5
5
|
by_framework_langgraph/worker.py,sha256=wFalpBQLqFm-YzJugzAzkA3ciI9lUzxRTv4GmqVCv78,6156
|
|
6
|
-
by_framework_langgraph-0.0.3.
|
|
7
|
-
by_framework_langgraph-0.0.3.
|
|
8
|
-
by_framework_langgraph-0.0.3.
|
|
6
|
+
by_framework_langgraph-0.0.3.dev4.dist-info/METADATA,sha256=48vOtcoLYGH-XS9TT6Jg5D1CzxX7ro6auzAi8TlhGHk,3939
|
|
7
|
+
by_framework_langgraph-0.0.3.dev4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
by_framework_langgraph-0.0.3.dev4.dist-info/RECORD,,
|