Skip to content

Commit ce2b851

Browse files
committed
Add patch_abc utility
1 parent 64de448 commit ce2b851

File tree

4 files changed

+225
-0
lines changed

4 files changed

+225
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""
2+
LangChain ABC Patching Example
3+
4+
This module demonstrates how to use `patch_abc` to instrument multiple LangChain language model implementations without
5+
needing to patch each provider individually.
6+
7+
Overview:
8+
- Uses `patch_abc` to instrument LangChain's BaseLLM and BaseChatModel abstract base classes
9+
- Automatically applies instrumentation to all concrete implementations (OpenAI, Ollama, HuggingFace)
10+
- Provides a CLI interface to switch between different model providers at runtime
11+
- Demonstrates runtime polymorphism with a `chat_with_model` function
12+
13+
Supported Providers:
14+
- OpenAI (ChatOpenAI) - inherits from BaseChatModel
15+
- Ollama (OllamaLLM) - inherits from BaseLLM
16+
- HuggingFace (HuggingFaceEndpoint) - inherits from BaseLLM
17+
18+
Important Notes:
19+
- Model imports must happen BEFORE patching for the instrumentation to work
20+
- OpenAI models call _generate twice, hence the dual patching approach
21+
- The wrapper captures and logs all arguments/kwargs passed to the _generate method
22+
23+
Usage:
24+
python langchain_example.py --provider ollama
25+
python langchain_example.py --provider openai --model gpt-4
26+
python langchain_example.py --provider huggingface --prompt "Custom question"
27+
"""
28+
import argparse
29+
30+
from langchain_core.language_models import BaseLLM, BaseChatModel
31+
from langchain_core.language_models.base import BaseLanguageModel
32+
33+
# important note: if you import these after patching, the patch won't apply!
34+
# mitigation will be added to patch_abc in a future release
35+
from langchain_huggingface import HuggingFaceEndpoint
36+
from langchain_ollama import OllamaLLM
37+
from langchain_openai import ChatOpenAI
38+
39+
from opentelemetry.util._wrap import patch_abc
40+
41+
42+
def parse_args():
43+
parser = argparse.ArgumentParser(description="LangChain model comparison")
44+
parser.add_argument("--provider", choices=["ollama", "openai", "huggingface"], default="ollama",
45+
help="Choose model provider (default: ollama)")
46+
parser.add_argument("--model", type=str, help="Specify model name")
47+
parser.add_argument("--prompt", type=str, default="What is the capital of France?",
48+
help="Input prompt")
49+
50+
return parser.parse_args()
51+
52+
53+
def chat_with_model(model: BaseLanguageModel, prompt: str) -> str:
54+
try:
55+
response = model.invoke(prompt)
56+
if hasattr(response, 'content'):
57+
return response.content
58+
else:
59+
return str(response)
60+
except Exception as e:
61+
return f"Error: {str(e)}"
62+
63+
64+
def create_huggingface_model(model: str = "google/flan-t5-small"):
65+
return HuggingFaceEndpoint(
66+
repo_id=model,
67+
temperature=0.7
68+
)
69+
70+
71+
def create_openai_model(model: str = "gpt-3.5-turbo"):
72+
return ChatOpenAI(
73+
model=model,
74+
temperature=0.7
75+
)
76+
77+
78+
def create_ollama_model(model: str = "llama2"):
79+
return OllamaLLM(
80+
model=model,
81+
temperature=0.7
82+
)
83+
84+
85+
def patch_llm():
86+
def my_wrapper(orig_fcn):
87+
def wrapped_fcn(self, *args, **kwargs):
88+
print("wrapper starting")
89+
print(f"Arguments: {args}")
90+
print(f"Keyword arguments: {kwargs}")
91+
return orig_fcn(self, *args, **kwargs)
92+
93+
return wrapped_fcn
94+
95+
patch_abc(BaseLLM, "_generate", my_wrapper)
96+
97+
# this is for OpenAI, which is a weird case. The _generate method is in a differnt base class and gets called twice.
98+
patch_abc(BaseChatModel, "_generate", my_wrapper)
99+
100+
101+
def main():
102+
args = parse_args()
103+
104+
patch_llm()
105+
106+
if args.provider == "ollama":
107+
model = create_ollama_model(args.model or "llama2")
108+
elif args.provider == "openai":
109+
model = create_openai_model(args.model or "gpt-3.5-turbo")
110+
elif args.provider == "huggingface":
111+
model = create_huggingface_model(args.model or "google/flan-t5-small")
112+
else:
113+
raise ValueError(f"Unsupported provider: {args.provider}")
114+
115+
response = chat_with_model(model, args.prompt)
116+
print(f"{args.provider.title()} Response: {response}")
117+
118+
119+
if __name__ == "__main__":
120+
main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
langchain-core
2+
langchain-openai
3+
langchain-ollama
4+
langchain-huggingface
5+
huggingface-hub
6+
opentelemetry-api
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Simple example demonstrating the abstract base class patching utility.
3+
4+
This module shows how to use the `patch_abc` function to instrument all implementations
5+
of an abstract base class without needing to patch each concrete class individually.
6+
7+
Key Concepts:
8+
- Defines an abstract `Greeter` class with an abstract `greet` method
9+
- Creates two concrete implementations: `EngishGreeter` and `SpanishGreeter`
10+
- Uses `patch_abc` to wrap the abstract `greet` method with instrumentation
11+
- The wrapper automatically applies to all subclasses that implement the abstract method
12+
13+
When run, this example demonstrates that:
14+
1. A single call to `patch_abc` on the base class instruments all subclasses
15+
2. The wrapper executes before and after each concrete implementation
16+
3. This provides a powerful way to add telemetry/logging to entire class hierarchies
17+
18+
Output:
19+
wrapper running
20+
hello
21+
wrapper running
22+
hola
23+
"""
24+
from abc import ABC, abstractmethod
25+
26+
from opentelemetry.util._wrap import patch_abc
27+
28+
29+
class Greeter(ABC):
30+
@abstractmethod
31+
def greet(self):
32+
pass
33+
34+
35+
class EngishGreeter(Greeter):
36+
def greet(self):
37+
print("hello")
38+
39+
40+
class SpanishGreeter(Greeter):
41+
def greet(self):
42+
print("hola")
43+
44+
45+
if __name__ == '__main__':
46+
def my_wrapper(orig_fcn):
47+
def wrapped_fcn(self, *args, **kwargs):
48+
print("wrapper running")
49+
result = orig_fcn(self, *args, **kwargs)
50+
return result
51+
52+
return wrapped_fcn
53+
54+
55+
patch_abc(Greeter, "greet", my_wrapper)
56+
57+
EngishGreeter().greet()
58+
SpanishGreeter().greet()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
def patch_abc(abstract_base_class, method_name, w):
2+
"""
3+
Monkey patches a method across all subclasses of an abstract base class.
4+
5+
This function finds all concrete subclasses of the given abstract base class
6+
and applies the provided wrapper function to the specified method in each subclass.
7+
8+
This approach is necessary because you can't directly monkey patch an abstract
9+
method in a base class -- concrete implementations in subclasses won't inherit
10+
the patched behavior since they override the abstract method. Instead, we
11+
individually patch each implementation in each concrete subclass.
12+
13+
Args:
14+
abstract_base_class: The abstract base class whose subclasses will be patched
15+
method_name: Name of the method to patch
16+
w: A wrapper function that takes the original method as an argument
17+
and returns a new function to replace it
18+
19+
Example:
20+
patch_abc(Greeter, "greet", my_wrapper)
21+
"""
22+
subclasses = recursively_get_all_subclasses(abstract_base_class)
23+
for subclass in subclasses:
24+
old_method = getattr(subclass, method_name)
25+
setattr(subclass, method_name, w(old_method))
26+
27+
# This implementation does not work if the instrumented class is imported after the instrumentor runs.
28+
# However, that case can be handled by querying the gc module for all existing classes; this capability can be added
29+
# in a follow-up release.
30+
31+
32+
def recursively_get_all_subclasses(cls):
33+
out = set()
34+
for subclass in cls.__subclasses__():
35+
out.add(subclass)
36+
out.update(recursively_get_all_subclasses(subclass))
37+
return out
38+
39+
40+
if __name__ == "__main__":
41+
pass

0 commit comments

Comments
 (0)