[SOLVED] LangGraph plots incorrectly the workflow #211

Closed
opened 2026-02-20 17:31:56 -05:00 by yindo · 6 comments
Owner

Originally created by @Nachoeigu on GitHub (Aug 26, 2024).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph/LangChain documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph/LangChain rather than my code.
  • I am sure this is better as an issue rather than a GitHub discussion, since this is a LangGraph bug and not a design question.

Example Code

import os
from dotenv import load_dotenv
import sys

load_dotenv()
WORKDIR=os.getenv("WORKDIR")
os.chdir(WORKDIR)
sys.path.append(WORKDIR)

from langchain_google_genai.chat_models import ChatGoogleGenerativeAI
from langchain_openai.chat_models import ChatOpenAI
from langgraph.graph import StateGraph
from src.utils import State, GraphInput, GraphOutput, GraphConfig, check_chapter
from src.nodes import *
from src.routers import *
from langgraph.graph import END
from src.utils import State

def should_go_to_brainstorming_writer(state: State):
    if state.get('instructor_documents', '') == '':
        return "human_feedback"
    else:
        return "brainstorming_writer"
    
def should_continue_with_critique(state: State):
    if state.get('is_plan_approved', None) is None: 
        return "brainstorming_critique"
    elif state['is_plan_approved'] == True:
        return "writer"
    else:
        return "brainstorming_critique"
    
def has_writer_ended_book(state: State):
    if state['current_chapter'] == len(state['chapters_summaries']):
        return END
    else:
        return "writer"

workflow = StateGraph(State, 
                      input = GraphInput,
                      config_schema = GraphConfig)

workflow.add_node("instructor", get_clear_instructions)
workflow.set_entry_point("instructor")
workflow.add_node("human_feedback", read_human_feedback)
workflow.add_node("brainstorming_writer", making_writer_brainstorming)
workflow.add_node("brainstorming_critique", brainstorming_critique)
workflow.add_node("writer", generate_content)
workflow.add_conditional_edges(
    "instructor",
    should_go_to_brainstorming_writer
)
workflow.add_edge("human_feedback","instructor")
workflow.add_conditional_edges(
    "brainstorming_writer",
    should_continue_with_critique
)

workflow.add_edge("brainstorming_critique","brainstorming_writer")
workflow.add_conditional_edges(
    "writer",
    has_writer_ended_book
)

app = workflow.compile(
    interrupt_before=['human_feedback']
    )

Error Message and Stack Trace (if applicable)

No response

Description

LangGraph plots my workflow in the following way, which is not correct:
image

The workflow should look like:

Screenshot 2024-08-26 at 10 39 09

It created relationships where there are no ones.

System Info

langchain 0.2.14
langchain-community 0.2.12
langchain-core 0.2.34
langchain-google-genai 1.0.10
langchain-groq 0.1.9
langchain-openai 0.1.22
langchain-text-splitters 0.2.2
langgraph 0.2.14
langgraph-checkpoint 1.0.6
langsmith 0.1.104

Originally created by @Nachoeigu on GitHub (Aug 26, 2024). ### Checked other resources - [X] I added a very descriptive title to this issue. - [X] I searched the [LangGraph](https://langchain-ai.github.io/langgraph/)/LangChain documentation with the integrated search. - [X] I used the GitHub search to find a similar question and didn't find it. - [X] I am sure that this is a bug in LangGraph/LangChain rather than my code. - [X] I am sure this is better as an issue [rather than a GitHub discussion](https://github.com/langchain-ai/langgraph/discussions/new/choose), since this is a LangGraph bug and not a design question. ### Example Code ```python import os from dotenv import load_dotenv import sys load_dotenv() WORKDIR=os.getenv("WORKDIR") os.chdir(WORKDIR) sys.path.append(WORKDIR) from langchain_google_genai.chat_models import ChatGoogleGenerativeAI from langchain_openai.chat_models import ChatOpenAI from langgraph.graph import StateGraph from src.utils import State, GraphInput, GraphOutput, GraphConfig, check_chapter from src.nodes import * from src.routers import * from langgraph.graph import END from src.utils import State def should_go_to_brainstorming_writer(state: State): if state.get('instructor_documents', '') == '': return "human_feedback" else: return "brainstorming_writer" def should_continue_with_critique(state: State): if state.get('is_plan_approved', None) is None: return "brainstorming_critique" elif state['is_plan_approved'] == True: return "writer" else: return "brainstorming_critique" def has_writer_ended_book(state: State): if state['current_chapter'] == len(state['chapters_summaries']): return END else: return "writer" workflow = StateGraph(State, input = GraphInput, config_schema = GraphConfig) workflow.add_node("instructor", get_clear_instructions) workflow.set_entry_point("instructor") workflow.add_node("human_feedback", read_human_feedback) workflow.add_node("brainstorming_writer", making_writer_brainstorming) workflow.add_node("brainstorming_critique", brainstorming_critique) workflow.add_node("writer", generate_content) workflow.add_conditional_edges( "instructor", should_go_to_brainstorming_writer ) workflow.add_edge("human_feedback","instructor") workflow.add_conditional_edges( "brainstorming_writer", should_continue_with_critique ) workflow.add_edge("brainstorming_critique","brainstorming_writer") workflow.add_conditional_edges( "writer", has_writer_ended_book ) app = workflow.compile( interrupt_before=['human_feedback'] ) ``` ### Error Message and Stack Trace (if applicable) _No response_ ### Description LangGraph plots my workflow in the following way, which is not correct: <img width="722" alt="image" src="https://github.com/user-attachments/assets/239b422e-8a74-4c83-b977-ba908d1b29e3"> The workflow should look like: ![Screenshot 2024-08-26 at 10 39 09](https://github.com/user-attachments/assets/f013c3e8-2d2c-4166-b970-2ee88f383c3a) It created relationships where there are no ones. ### System Info langchain 0.2.14 langchain-community 0.2.12 langchain-core 0.2.34 langchain-google-genai 1.0.10 langchain-groq 0.1.9 langchain-openai 0.1.22 langchain-text-splitters 0.2.2 langgraph 0.2.14 langgraph-checkpoint 1.0.6 langsmith 0.1.104
yindo closed this issue 2026-02-20 17:31:57 -05:00
Author
Owner
Author
Owner

@Nachoeigu commented on GitHub (Aug 26, 2024):

Thank you very much, I couldn´t find them in my research.

I think a good improvement in the framework could be that automaticaly detects the return possible values of the router and, based on it, make the relationships. What do you think?

@Nachoeigu commented on GitHub (Aug 26, 2024): > * [`add_conditional_edges` does not work without optional argument `path_map` #987](https://github.com/langchain-ai/langgraph/issues/987) > * [Visualized graph has wrong conditional edges #1394](https://github.com/langchain-ai/langgraph/issues/1394) Thank you very much, I couldn´t find them in my research. I think a good improvement in the framework could be that automaticaly detects the return possible values of the router and, based on it, make the relationships. What do you think?
Author
Owner

@gbaian10 commented on GitHub (Aug 26, 2024):

Thank you very much, I couldn´t find them in my research.

I think a good improvement in the framework could be that automaticaly detects the return possible values of the router and, based on it, make the relationships. What do you think?

It seems to be mentioned here.
https://langchain-ai.github.io/langgraph/cloud/faq/studio/?h=router#why-are-extra-edges-showing-up-in-my-graph

If it could automatically detect and complete, that would be even better. After all, many people have raised the same issue.
At least I'm sure VSCode can automatically detect return types, which proves it's feasible from a programming perspective. I'm just not sure how difficult it would be to implement.

image

image

image

@gbaian10 commented on GitHub (Aug 26, 2024): > > * [`add_conditional_edges` does not work without optional argument `path_map` #987](https://github.com/langchain-ai/langgraph/issues/987) > > * [Visualized graph has wrong conditional edges #1394](https://github.com/langchain-ai/langgraph/issues/1394) > > Thank you very much, I couldn´t find them in my research. > > I think a good improvement in the framework could be that automaticaly detects the return possible values of the router and, based on it, make the relationships. What do you think? It seems to be mentioned here. https://langchain-ai.github.io/langgraph/cloud/faq/studio/?h=router#why-are-extra-edges-showing-up-in-my-graph If it could automatically detect and complete, that would be even better. After all, many people have raised the same issue. At least I'm sure VSCode can automatically detect return types, which proves it's feasible from a programming perspective. I'm just not sure how difficult it would be to implement. ![image](https://github.com/user-attachments/assets/b4d51097-acb8-4820-99a8-01f9cb2aab1d) ![image](https://github.com/user-attachments/assets/ec31d4b9-6837-4595-a93e-38ac068515da) ![image](https://github.com/user-attachments/assets/ebb0bbb7-939e-42f8-ad50-2a88c1d99fa1)
Author
Owner

@Nachoeigu commented on GitHub (Aug 26, 2024):

Thank you very much, I couldn´t find them in my research.
I think a good improvement in the framework could be that automaticaly detects the return possible values of the router and, based on it, make the relationships. What do you think?

It seems to be mentioned here. https://langchain-ai.github.io/langgraph/cloud/faq/studio/?h=router#why-are-extra-edges-showing-up-in-my-graph

If it could automatically detect and complete, that would be even better. After all, many people have raised the same issue. At least I'm sure VSCode can automatically detect return types, which proves it's feasible from a programming perspective. I'm just not sure how difficult it would be to implement.

image

image

image

I think with ast something like that could be possible.

import ast
import inspect
from langgraph.graph import END
import pandas as pd

class ReturnValueExtractor(ast.NodeVisitor):
    """
    A class to extract all possible return expressions from a Python function.
    
    Attributes:
        return_expressions (set): A set of unique return expressions found in the function.
    """

    def __init__(self):
        """Initialize the ReturnValueExtractor with an empty set of return expressions."""
        self.return_expressions = set()

    def visit_Return(self, node):
        """
        Visit a return statement in the AST and extract the return expression.

        Args:
            node (ast.Return): The return node in the AST.
        """
        if node.value:
            # Convert the return expression to a string and add to the set
            self.return_expressions.add(ast.dump(node.value, annotate_fields=False))

        # Continue walking the AST
        self.generic_visit(node)

    def extract(self, func):
        """
        Extract all return expressions from the given function.

        Args:
            func (function): The function from which to extract return expressions.

        Returns:
            list: A list of unique return expressions as strings.
        """
        source = inspect.getsource(func)
        tree = ast.parse(source)
        self.visit(tree)
        
        return list(self.return_expressions)

# Example function to test
def example_function(state):
    if state.get('is_plan_approved', None) is None: 
        return "brainstorming_critique"
    elif state['is_plan_approved'] == True:
        return "writer"
    elif state['is_plan_approved'] == False:
        return "another_agent"
    else:
        return END


def complex_function(state):
    if state.get('is_ready', False):
        return END
    elif 1+1 == 3:
        return pd.DataFrame()
    else:
        return example_function
if __name__ == '__main__':
    extractor = ReturnValueExtractor()
    possible_returns = extractor.extract(example_function)
    print(possible_returns)
    print('--------------')
    possible_returns = extractor.extract(complex_function)
    print(possible_returns)

@Nachoeigu commented on GitHub (Aug 26, 2024): > > > * [`add_conditional_edges` does not work without optional argument `path_map` #987](https://github.com/langchain-ai/langgraph/issues/987) > > > * [Visualized graph has wrong conditional edges #1394](https://github.com/langchain-ai/langgraph/issues/1394) > > > > > > Thank you very much, I couldn´t find them in my research. > > I think a good improvement in the framework could be that automaticaly detects the return possible values of the router and, based on it, make the relationships. What do you think? > > It seems to be mentioned here. https://langchain-ai.github.io/langgraph/cloud/faq/studio/?h=router#why-are-extra-edges-showing-up-in-my-graph > > If it could automatically detect and complete, that would be even better. After all, many people have raised the same issue. At least I'm sure VSCode can automatically detect return types, which proves it's feasible from a programming perspective. I'm just not sure how difficult it would be to implement. > > ![image](https://private-user-images.githubusercontent.com/34255899/361379735-b4d51097-acb8-4820-99a8-01f9cb2aab1d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ2NjU1ODcsIm5iZiI6MTcyNDY2NTI4NywicGF0aCI6Ii8zNDI1NTg5OS8zNjEzNzk3MzUtYjRkNTEwOTctYWNiOC00ODIwLTk5YTgtMDFmOWNiMmFhYjFkLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI2VDA5NDEyN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWYwNzY3MjE4YWY0YjQ2MmZlNmJkNDg1ZWExYzM4MGIzNzIxNzRmOTRkM2U5NDNiOWFkMGVmZjEzNjQ5OTdkMzImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.S2Op0q52V3mtNlu2kaK1R8vk29L7Eq5VJoZiH3bdaqA) > > ![image](https://private-user-images.githubusercontent.com/34255899/361380066-ec31d4b9-6837-4595-a93e-38ac068515da.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ2NjU1ODcsIm5iZiI6MTcyNDY2NTI4NywicGF0aCI6Ii8zNDI1NTg5OS8zNjEzODAwNjYtZWMzMWQ0YjktNjgzNy00NTk1LWE5M2UtMzhhYzA2ODUxNWRhLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI2VDA5NDEyN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTgwOTY3Njk0OTYwMzg2NWYzMjMwNzEzNGJjNzY5ZDg5ZjU0NDA0ZDIzMDI3ODU1ODE5ZWZiODYzMmE5OTlhNTYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.HUa0cTqfNbuQj_zHEBHcN_tyPHX5mpb_a4zYFzOWqE8) > > ![image](https://private-user-images.githubusercontent.com/34255899/361380081-ebb0bbb7-939e-42f8-ad50-2a88c1d99fa1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQ2NjU1ODcsIm5iZiI6MTcyNDY2NTI4NywicGF0aCI6Ii8zNDI1NTg5OS8zNjEzODAwODEtZWJiMGJiYjctOTM5ZS00MmY4LWFkNTAtMmE4OGMxZDk5ZmExLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MjYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODI2VDA5NDEyN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTJkYzUwMDZiNTQ5ZWM4NmMwZWViN2E5MTI5YzQyMWJhZDM2MzlmM2Y1MDVhMTgxOTYxNTYzODFlYzc2NWQyNTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.yjNNRznqBz7XuSe7qnYYNxQgT9H-2Qra09SVCuEMnLU) I think with ast something like that could be possible. ``` import ast import inspect from langgraph.graph import END import pandas as pd class ReturnValueExtractor(ast.NodeVisitor): """ A class to extract all possible return expressions from a Python function. Attributes: return_expressions (set): A set of unique return expressions found in the function. """ def __init__(self): """Initialize the ReturnValueExtractor with an empty set of return expressions.""" self.return_expressions = set() def visit_Return(self, node): """ Visit a return statement in the AST and extract the return expression. Args: node (ast.Return): The return node in the AST. """ if node.value: # Convert the return expression to a string and add to the set self.return_expressions.add(ast.dump(node.value, annotate_fields=False)) # Continue walking the AST self.generic_visit(node) def extract(self, func): """ Extract all return expressions from the given function. Args: func (function): The function from which to extract return expressions. Returns: list: A list of unique return expressions as strings. """ source = inspect.getsource(func) tree = ast.parse(source) self.visit(tree) return list(self.return_expressions) # Example function to test def example_function(state): if state.get('is_plan_approved', None) is None: return "brainstorming_critique" elif state['is_plan_approved'] == True: return "writer" elif state['is_plan_approved'] == False: return "another_agent" else: return END def complex_function(state): if state.get('is_ready', False): return END elif 1+1 == 3: return pd.DataFrame() else: return example_function if __name__ == '__main__': extractor = ReturnValueExtractor() possible_returns = extractor.extract(example_function) print(possible_returns) print('--------------') possible_returns = extractor.extract(complex_function) print(possible_returns) ```
Author
Owner

@gbaian10 commented on GitHub (Aug 26, 2024):

If you include some situations where None is implicitly returned, the problem becomes very complex.
It will be much simpler if you only capture all explicitly returned values to check if they are Liteval, leaving the rest to the user.

def add(a: int, b: int):
    if a > b:
        raise ValueError("a should be less than b")
    if a == 1:
        return 1
    if b == 2:
        return "2"
    # return a + b

For example, it might return None, but you cannot easily complete this check just by getting the return value; more complex validation is required.

@gbaian10 commented on GitHub (Aug 26, 2024): If you include some situations where None is implicitly returned, the problem becomes very complex. It will be much simpler if you only capture all explicitly returned values to check if they are Liteval, leaving the rest to the user. ```py def add(a: int, b: int): if a > b: raise ValueError("a should be less than b") if a == 1: return 1 if b == 2: return "2" # return a + b ``` For example, it might return None, but you cannot easily complete this check just by getting the return value; more complex validation is required.
Author
Owner

@gbaian10 commented on GitHub (Aug 26, 2024):

import ast
import inspect
from collections.abc import Callable
from typing import Any


def is_literal_return(func: Callable) -> bool:
    """
    Check if the function only returns constant values.

    Args:
        func (Callable): The function to analyze.

    Returns:
        bool: True if all return statements are constant values, False otherwise.
    """
    source = inspect.getsource(func)
    tree = ast.parse(source)
    return all(
        isinstance(node.value, ast.Constant)
        for node in ast.walk(tree)
        if isinstance(node, ast.Return)
    )


def has_explicit_return(func: Callable) -> bool:
    """
    Check if all code paths in the function have an explicit return statement.

    Args:
        func (Callable): The function to analyze.

    Returns:
        bool: True if all paths have explicit return statements, False otherwise.
    """
    source = inspect.getsource(func)
    tree = ast.parse(source)
    function_defs = [
        node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
    ]
    if not function_defs:
        return False
    return check_all_paths_return(function_defs[0].body)


def check_all_paths_return(statements: list) -> bool:
    """
    Recursively check if all paths in the given statements have a return statement.

    Args:
        statements (list): A list of AST nodes representing statements.

    Returns:
        bool: True if all paths have return statements, False otherwise.
    """
    has_return = False
    for stmt in statements:
        if isinstance(stmt, ast.Return | ast.Raise):
            has_return = True
        elif isinstance(stmt, ast.If):
            has_return = check_all_paths_return(stmt.body) and check_all_paths_return(
                stmt.orelse
            )
        elif isinstance(stmt, ast.While | ast.For):
            continue
    return has_return


def analyze_function(func: Callable) -> set[Any] | None:
    """
    Analyze the function to determine if it returns a set of constant values.

    Args:
        func (Callable): The function to analyze.

    Returns:
        Optional[Set[Any]]: A set of constant values if the function meets the criteria, None otherwise.
    """
    if not is_literal_return(func) or not has_explicit_return(func):
        return None
    source = inspect.getsource(func)
    tree = ast.parse(source)
    return {node.value.value for node in ast.walk(tree) if isinstance(node, ast.Return)}


# Test examples
def example_func() -> int:
    if True:
        return 42
    return 42


def add(a: int, b: int):
    if a > b:
        raise ValueError("a should be less than b")
    if a == 1:
        return 1
    if b == 2:
        return "2"
    # return a + b


def ok():
    return "ok"


def no_ok():
    while 1 + 1 == 3:
        return 1


def one():
    return 1


# Usage examples
print(analyze_function(example_func))  # {42}
print(analyze_function(add))  # None
print(analyze_function(ok))  # {'ok'}
print(analyze_function(no_ok))  # None
print(analyze_function(one))  # {1}

I used an LLM to generate this code and I'm not sure if it covers all scenarios. Since I rarely use AST, I haven't fully reviewed its logic to ensure it's correct.

I expect the following order of rules:

  1. If path_map is included, use the path provided by path_map as a priority.
  2. If path_map is not included, read the return type hint of the path function.
  3. If neither is included, execute AST to check if the return value always returns a Liteval result in all possible cases. If so, use the set or list of that Liteval result.
  4. If it returns None, or if AST checking is disabled, issue a warning indicating that this point will lead to all points.

Three and four are new, but three would be a breaking change, so it's not suitable for a minor release unless the default is set to False.

@gbaian10 commented on GitHub (Aug 26, 2024): ```py import ast import inspect from collections.abc import Callable from typing import Any def is_literal_return(func: Callable) -> bool: """ Check if the function only returns constant values. Args: func (Callable): The function to analyze. Returns: bool: True if all return statements are constant values, False otherwise. """ source = inspect.getsource(func) tree = ast.parse(source) return all( isinstance(node.value, ast.Constant) for node in ast.walk(tree) if isinstance(node, ast.Return) ) def has_explicit_return(func: Callable) -> bool: """ Check if all code paths in the function have an explicit return statement. Args: func (Callable): The function to analyze. Returns: bool: True if all paths have explicit return statements, False otherwise. """ source = inspect.getsource(func) tree = ast.parse(source) function_defs = [ node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) ] if not function_defs: return False return check_all_paths_return(function_defs[0].body) def check_all_paths_return(statements: list) -> bool: """ Recursively check if all paths in the given statements have a return statement. Args: statements (list): A list of AST nodes representing statements. Returns: bool: True if all paths have return statements, False otherwise. """ has_return = False for stmt in statements: if isinstance(stmt, ast.Return | ast.Raise): has_return = True elif isinstance(stmt, ast.If): has_return = check_all_paths_return(stmt.body) and check_all_paths_return( stmt.orelse ) elif isinstance(stmt, ast.While | ast.For): continue return has_return def analyze_function(func: Callable) -> set[Any] | None: """ Analyze the function to determine if it returns a set of constant values. Args: func (Callable): The function to analyze. Returns: Optional[Set[Any]]: A set of constant values if the function meets the criteria, None otherwise. """ if not is_literal_return(func) or not has_explicit_return(func): return None source = inspect.getsource(func) tree = ast.parse(source) return {node.value.value for node in ast.walk(tree) if isinstance(node, ast.Return)} # Test examples def example_func() -> int: if True: return 42 return 42 def add(a: int, b: int): if a > b: raise ValueError("a should be less than b") if a == 1: return 1 if b == 2: return "2" # return a + b def ok(): return "ok" def no_ok(): while 1 + 1 == 3: return 1 def one(): return 1 # Usage examples print(analyze_function(example_func)) # {42} print(analyze_function(add)) # None print(analyze_function(ok)) # {'ok'} print(analyze_function(no_ok)) # None print(analyze_function(one)) # {1} ``` I used an LLM to generate this code and I'm not sure if it covers all scenarios. Since I rarely use AST, I haven't fully reviewed its logic to ensure it's correct. I expect the following order of rules: 1. If path_map is included, use the path provided by path_map as a priority. 1. If path_map is not included, read the return type hint of the path function. 1. If neither is included, execute AST to check if the return value always returns a Liteval result in all possible cases. If so, use the set or list of that Liteval result. 1. If it returns None, or if AST checking is disabled, issue a warning indicating that this point will lead to all points. Three and four are new, but three would be a breaking change, so it's not suitable for a minor release unless the default is set to False.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#211