Passing config to both tool in ParallelRunnable

@tool 
def search_web(query: str, config: RunnableConfig | None = None):
    """ 
    This tool performs a web search using an external search API to retrieve relevant information based on a user's query.
    
    Parameters:
    query: (string) The user's search query to find relevant web information.
    Return: A list of up to 5 web search results, each containing a title, snippet, and URL.
    """
    try:
        flag = 1 if config else 0
        print(f"if config: {flag}")

        if config:
            metadata = config.get('metadata', {})
            search_domains = metadata.get('search_domain', '')
        else:
            search_domains = ''
            print(f"🔍 No specific search domains provided in config: {config}")
        print(f"🔍 Performing web search for query: {query} with domains: {search_domains}")
        
        docs = []

        if search_domains:
            if "," in search_domains:
                search_domains = [d.strip() for d in search_domains.split(",")]
            else:
                search_domains = [search_domains.strip()]
            tavily_search = TavilySearch(
                    max_results=3,
                    search_depth="basic",
                    include_answer=False,
                    include_raw_content=False,
                    include_domains=search_domains
                )
            search_results = tavily_search.invoke({"query": query})
            results = search_results.get("results", [])
            for result in results:
                content = result.get("content", "")
                content = content
                url = result.get("url", "")
                docs.append(f"Web Search Result: {content} URL: {url} \n")
        
        return docs
        
    except Exception as e:
        print(f"❌ Error in web search: {e}")
        return []

@tool 
def search_vectorstore(query:str, config: RunnableConfig = None):
    """ 
    This tool retrieves relevant documents from a vector store based on a user's query.
    
    Parameters:
    query: (string) The user's search query to find matching documents.
    Return: A list of up to 7 documents that match the query.
    """
    start_time = time.time()
        
        # Get database and branch info from config
    flag = 1 if config else 0
    print(f"if config: {flag}")
    if config:
        if config.get('metadata'):
            metadata = config.get('metadata', {})
            database_name = metadata.get('database_name', 'PHARMCARE_HIA')
            branch_id = metadata.get('branch_id', '1')
        elif config.get("configurable"):
            configurable = config.get("configurable", {})
            database_name = configurable.get('database_name', 'PHARMCARE_HIA')
            branch_id = configurable.get('branch_id', '1')
    else:
        database_name = 'PHARMCARE_HIA'
        branch_id = '1'
    
    collection_name = f"{database_name}-{branch_id}"
    print(f"🔍 Searching all documents in collection: {collection_name}")
    
    try:
        # Use cached clients from connection pools
        genai_client = get_cached_genai_client("pharmcare-chatbot-429003", "us-central1")
        qdrant_client = get_cached_qdrant_client()
        embedding = VertexAIEmbeddings(model_name="gemini-embedding-001", project="pharmcare-chatbot-429003")
        
        if not genai_client:
            print("❌ Failed to get GenAI client from pool")
            return []
        
        if not qdrant_client:
            print("❌ Failed to get Qdrant client from pool")
            return []
        
        # Generate embedding using cached GenAI client
        embedding_start = time.time()
        resp = embedding.embed(texts=[query], dimensions=768, embeddings_task_type="RETRIEVAL_QUERY")
        embedding_time = time.time() - embedding_start

        query_embedding = resp[0]
        if not query_embedding:
            print("❌ Failed to get embedding for query")
            return []
        
        # Search using cached Qdrant client - EXCLUDE Handover documents and empty content
        search_start = time.time()
        # Create filter to exclude Handover category (system documents) and empty page_content
        filter_conditions = models.Filter(
            must=[
                models.FieldCondition(
                    key="page_content",
                    match=models.MatchExcept(**{"except": ["", "None", "none", "null"]})
                )
            ],
        )
        
        search_result = qdrant_client.search(
            collection_name=collection_name,
            query_vector=query_embedding,
            with_payload=True,
            limit=7,  # Retrieve results excluding Handover
            query_filter=filter_conditions  # Apply exclusion filter
        )
        search_time = time.time() - search_start
        
        # Format results with category information
        docs = []
        for point in search_result:
            metadata = point.payload.get('metadata', {})
            category = metadata.get('category', '')
            source = metadata.get('source', 'N/A')
            content = point.payload.get('page_content', '')
            
            # Skip documents with empty or None content
            if not content or content.strip() == '' or content.lower() == 'none':
                print(f"⚠️ Skipping document with empty content: category={category}, source={source}")
                continue
            
            # Skip Handover documents (additional safety check)
            # if category == 'Handover':
            #     print(f"⚠️ Skipping Handover document: {source}")
            #     continue
            
            # Format based on category
            if category == 'template':
                question = metadata.get('question', 'N/A')
                docs.append(f"[Template] Question: {question}\nContent: {content}")
            elif category == 'QnA':
                question = metadata.get('question', 'N/A')
                docs.append(f"[QnA] Question: {question}\nAnswer: {content}")
            elif category == 'Handover':
                question = metadata.get('question', 'N/A')
                docs.append(f"[QnA] Question: {question}\nAnswer: {content}")
            else:
                docs.append(f"[{category}] Source: {source}\nContent: {content}")
        
        total_time = time.time() - start_time
        print(f"âś… Retrieved {len(docs)} documents in {total_time:.3f}s (embedding: {embedding_time:.3f}s, search: {search_time:.3f}s)")
        
        # Debug: Print first result if available
        # if docs:
        #     print(f"đź“„ Sample result: {docs[0][:200]}...")
        # else:
        #     print("⚠️ No valid documents found after filtering")
    except Exception as e:
        print(f"❌ Error in vectorstore search: {e}")
        docs = []
    return docs        

@tool
def search_all_documents(query: str, config: RunnableConfig = None):
    """ 
    This tool retrieves relevant documents from a vector store based on a user's query.
    Uses connection pools for optimal performance across multiple invocations.
    
    Parameters:
    query: (string) The user's search query to find matching documents.
    Return: A list of up to 7 documents that match the query across all user-facing categories.
    Constraint: Excludes system documents (Handover category) and documents with empty content.
              Searches templates, QnA, KM, web_crawl, and other user-facing content.
    """
    parallel_runnables = RunnableParallel({
        "km_search": RunnableLambda(lambda _: search_vectorstore.invoke({"query": query}, config=config)),
        "online_search": RunnableLambda(lambda _: search_web.invoke({"query": query}, config=config)),
    })

    results = parallel_runnables.invoke({})
    km_search = results.get("km_search", "")
    online_search = results.get("online_search", "")
    return km_search + online_search

I am running the above code via the agent system in langgraph. The agent is created by create_agent using llm, tools and prompts. However, i found that the config flag is 1 on only search_vectorstore but 0 on search_web. I have tried using RunnableLambda but the result is still the same.

hi @charlieckh

instead of that:

def search_web(query: str, config: RunnableConfig | None = None)

Try this:

def search_web(query: str, config: RunnableConfig = None)

IMHO, the difference is caused by the type annotation of the config parameter in your tools. LangChain only injects the runtime RunnableConfig into a tool function if the parameter is annotated exactly as RunnableConfig. If you annotate it as a union (e.g., RunnableConfig | None or Optional[RunnableConfig]), LangChain will not recognize it and will not pass the config into your function, so config will be None.

if you are using the latest updates v1 you can use runtime to access the config as well

from dataclasses import dataclass
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime

USER_DATABASE = {
“user123”: {
“name”: “Alice Johnson”,
“account_type”: “Premium”,
“balance”: 5000,
“email”: “alice@example.com”
},
“user456”: {
“name”: “Bob Smith”,
“account_type”: “Standard”,
“balance”: 1200,
“email”: “bob@example.com”
}
}

@dataclass
class UserContext:
user_id: str

@tool
def get_account_info(runtime: ToolRuntime[UserContext]) → str:
“”“Get the current user’s account information.”“”
user_id = runtime.context.user_id

if user_id in USER_DATABASE:
    user = USER_DATABASE[user_id]
    return f"Account holder: {user['name']}\nType: {user['account_type']}\nBalance: ${user['balance']}"
return "User not found"

model = ChatOpenAI(model=“gpt-4o”)
agent = create_agent(
model,
tools=[get_account_info],
context_schema=UserContext,
system_prompt=“You are a financial assistant.”
)

result = agent.invoke(
{“messages”: [{“role”: “user”, “content”: “What’s my current balance?”}]},
context=UserContext(user_id=“user123”)
)