langtrace-python-sdk 1.3.5__py3-none-any.whl → 1.3.6__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.
@@ -121,7 +121,8 @@ def async_images_generate(original_method, version, tracer):
121
121
  with tracer.start_as_current_span(
122
122
  APIS["IMAGES_GENERATION"]["METHOD"], kind=SpanKind.CLIENT
123
123
  ) as span:
124
- async for field, value in attributes.model_dump(by_alias=True).items():
124
+ items = attributes.model_dump(by_alias=True).items()
125
+ for field, value in items:
125
126
  if value is not None:
126
127
  span.set_attribute(field, value)
127
128
  try:
@@ -325,7 +326,9 @@ def chat_completions_create(original_method, version, tracer):
325
326
  span.end()
326
327
  raise
327
328
 
328
- def handle_streaming_response(result, span, prompt_tokens, function_call=False, tool_calls=False):
329
+ def handle_streaming_response(
330
+ result, span, prompt_tokens, function_call=False, tool_calls=False
331
+ ):
329
332
  """Process and yield streaming response chunks."""
330
333
  result_content = []
331
334
  span.add_event(Event.STREAM_START.value)
@@ -343,12 +346,16 @@ def chat_completions_create(original_method, version, tracer):
343
346
  content = [choice.delta.content]
344
347
  elif function_call:
345
348
  for choice in chunk.choices:
346
- if choice.delta and choice.delta.function_call and choice.delta.function_call.arguments is not None:
347
- token_counts = estimate_tokens(choice.delta.function_call.arguments)
348
- completion_tokens += token_counts
349
- content = [
349
+ if (
350
+ choice.delta
351
+ and choice.delta.function_call
352
+ and choice.delta.function_call.arguments is not None
353
+ ):
354
+ token_counts = estimate_tokens(
350
355
  choice.delta.function_call.arguments
351
- ]
356
+ )
357
+ completion_tokens += token_counts
358
+ content = [choice.delta.function_call.arguments]
352
359
  elif tool_calls:
353
360
  # TODO(Karthik): Tool calls streaming is tricky. The chunks after the
354
361
  # first one are missing the function name and id though the arguments
@@ -357,7 +364,14 @@ def chat_completions_create(original_method, version, tracer):
357
364
  else:
358
365
  content = []
359
366
  span.add_event(
360
- Event.STREAM_OUTPUT.value, {"response": "".join(content) if len(content) > 0 and content[0] is not None else ""}
367
+ Event.STREAM_OUTPUT.value,
368
+ {
369
+ "response": (
370
+ "".join(content)
371
+ if len(content) > 0 and content[0] is not None
372
+ else ""
373
+ )
374
+ },
361
375
  )
362
376
  result_content.append(content[0] if len(content) > 0 else "")
363
377
  yield chunk
@@ -559,7 +573,9 @@ def async_chat_completions_create(original_method, version, tracer):
559
573
  span.end()
560
574
  raise
561
575
 
562
- async def ahandle_streaming_response(result, span, prompt_tokens, function_call=False, tool_calls=False):
576
+ async def ahandle_streaming_response(
577
+ result, span, prompt_tokens, function_call=False, tool_calls=False
578
+ ):
563
579
  """Process and yield streaming response chunks."""
564
580
  result_content = []
565
581
  span.add_event(Event.STREAM_START.value)
@@ -577,12 +593,16 @@ def async_chat_completions_create(original_method, version, tracer):
577
593
  content = [choice.delta.content]
578
594
  elif function_call:
579
595
  for choice in chunk.choices:
580
- if choice.delta and choice.delta.function_call and choice.delta.function_call.arguments is not None:
581
- token_counts = estimate_tokens(choice.delta.function_call.arguments)
582
- completion_tokens += token_counts
583
- content = [
596
+ if (
597
+ choice.delta
598
+ and choice.delta.function_call
599
+ and choice.delta.function_call.arguments is not None
600
+ ):
601
+ token_counts = estimate_tokens(
584
602
  choice.delta.function_call.arguments
585
- ]
603
+ )
604
+ completion_tokens += token_counts
605
+ content = [choice.delta.function_call.arguments]
586
606
  elif tool_calls:
587
607
  # TODO(Karthik): Tool calls streaming is tricky. The chunks after the
588
608
  # first one are missing the function name and id though the arguments
@@ -591,7 +611,14 @@ def async_chat_completions_create(original_method, version, tracer):
591
611
  else:
592
612
  content = []
593
613
  span.add_event(
594
- Event.STREAM_OUTPUT.value, {"response": "".join(content) if len(content) > 0 and content[0] is not None else ""}
614
+ Event.STREAM_OUTPUT.value,
615
+ {
616
+ "response": (
617
+ "".join(content)
618
+ if len(content) > 0 and content[0] is not None
619
+ else ""
620
+ )
621
+ },
595
622
  )
596
623
  result_content.append(content[0] if len(content) > 0 else "")
597
624
  yield chunk
@@ -756,11 +783,19 @@ def async_embeddings_create(original_method, version, tracer):
756
783
 
757
784
  def extract_content(choice):
758
785
  # Check if choice.message exists and has a content attribute
759
- if hasattr(choice, 'message') and hasattr(choice.message, 'content') and choice.message.content is not None:
786
+ if (
787
+ hasattr(choice, "message")
788
+ and hasattr(choice.message, "content")
789
+ and choice.message.content is not None
790
+ ):
760
791
  return choice.message.content
761
792
 
762
793
  # Check if choice.message has tool_calls and extract information accordingly
763
- elif hasattr(choice, 'message') and hasattr(choice.message, 'tool_calls') and choice.message.tool_calls is not None:
794
+ elif (
795
+ hasattr(choice, "message")
796
+ and hasattr(choice.message, "tool_calls")
797
+ and choice.message.tool_calls is not None
798
+ ):
764
799
  result = [
765
800
  {
766
801
  "id": tool_call.id,
@@ -768,13 +803,18 @@ def extract_content(choice):
768
803
  "function": {
769
804
  "name": tool_call.function.name,
770
805
  "arguments": tool_call.function.arguments,
771
- }
772
- } for tool_call in choice.message.tool_calls
806
+ },
807
+ }
808
+ for tool_call in choice.message.tool_calls
773
809
  ]
774
810
  return result
775
811
 
776
812
  # Check if choice.message has a function_call and extract information accordingly
777
- elif hasattr(choice, 'message') and hasattr(choice.message, 'function_call') and choice.message.function_call is not None:
813
+ elif (
814
+ hasattr(choice, "message")
815
+ and hasattr(choice.message, "function_call")
816
+ and choice.message.function_call is not None
817
+ ):
778
818
  return {
779
819
  "name": choice.message.function_call.name,
780
820
  "arguments": choice.message.function_call.arguments,
@@ -1 +1 @@
1
- __version__ = "1.3.5"
1
+ __version__ = "1.3.6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: langtrace-python-sdk
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: Python SDK for LangTrace
5
5
  Project-URL: Homepage, https://github.com/Scale3-Labs/langtrace-python-sdk
6
6
  Author-email: Scale3 Labs <engineering@scale3labs.com>
@@ -25,6 +25,10 @@ Requires-Dist: langchain-openai; extra == 'dev'
25
25
  Requires-Dist: llama-index; extra == 'dev'
26
26
  Requires-Dist: openai; extra == 'dev'
27
27
  Requires-Dist: python-dotenv; extra == 'dev'
28
+ Provides-Extra: test
29
+ Requires-Dist: pytest; extra == 'test'
30
+ Requires-Dist: pytest-asyncio; extra == 'test'
31
+ Requires-Dist: pytest-vcr; extra == 'test'
28
32
  Description-Content-Type: text/markdown
29
33
 
30
34
  # [Langtrace](https://www.langtrace.ai)
@@ -30,7 +30,7 @@ examples/pinecone_example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
30
30
  examples/pinecone_example/basic.py,sha256=OkYjN3J5kxw-kloOV3Q-iyI6opkbarWsMom-_AMP2ZA,893
31
31
  langtrace_python_sdk/__init__.py,sha256=SlHg447-nQBbw8exRNJP_OyHUZ39Sldb7aaQ35hIRm8,262
32
32
  langtrace_python_sdk/langtrace.py,sha256=83-AkdASO7UF9FHR9BDZUSeYv9GFZkJJQD2YLKbqzo8,3562
33
- langtrace_python_sdk/version.py,sha256=tdqvkGH0OryRjjXzO3HS5DyYol-VTO9fC8m43nB2PgI,22
33
+ langtrace_python_sdk/version.py,sha256=5ZbAQtod5QalTI1C2N07edlxplzG_Q2XvGOSyOok4uA,22
34
34
  langtrace_python_sdk/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  langtrace_python_sdk/constants/exporter/langtrace_exporter.py,sha256=5MNjnAOg-4am78J3gVMH6FSwq5N8TOj72ugkhsw4vi0,46
36
36
  langtrace_python_sdk/constants/instrumentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -66,14 +66,31 @@ langtrace_python_sdk/instrumentation/llamaindex/instrumentation.py,sha256=D7_HPv
66
66
  langtrace_python_sdk/instrumentation/llamaindex/patch.py,sha256=8IM2dedF81w8_vVyA56JptyvlQl_bQO4UcB56sptuGs,3700
67
67
  langtrace_python_sdk/instrumentation/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
68
  langtrace_python_sdk/instrumentation/openai/instrumentation.py,sha256=Pv4n4z_kSxvZGVxrj3AopBoWQSxIOtMKolkxHrchRdM,2162
69
- langtrace_python_sdk/instrumentation/openai/patch.py,sha256=ZxR6hLXbf05rw7tccNsEQv-uB8Zb8QvOzTJk1coJbkY,33819
69
+ langtrace_python_sdk/instrumentation/openai/patch.py,sha256=ou8_48lmHmtI9UTU--QVl7KaAIeBnNtqOuHtnUQqjpU,34590
70
70
  langtrace_python_sdk/instrumentation/pinecone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  langtrace_python_sdk/instrumentation/pinecone/instrumentation.py,sha256=o0EUd5jvHaDKOUTj4NjnL5UfDHDHxyXkWGlTW4oeRDk,1784
72
72
  langtrace_python_sdk/instrumentation/pinecone/patch.py,sha256=5lF7hQmg2-U2EWtOC0w8_peRaNMysBomb0fjiNoS6eQ,2200
73
73
  langtrace_python_sdk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
74
  langtrace_python_sdk/utils/llm.py,sha256=4z2e-md_ELXCEuOIRVWracR6qH2pmsOxCqpkuF9_3Nw,1589
75
75
  langtrace_python_sdk/utils/with_root_span.py,sha256=N7ONrcF0myZbHBy5gpQffDbX-Kf63Crsz9szG0i3m08,1889
76
- langtrace_python_sdk-1.3.5.dist-info/METADATA,sha256=yUAeoRPdsjv_i4VUE0lwzH10O1uNWt9-S-qj8yMhRAI,9094
77
- langtrace_python_sdk-1.3.5.dist-info/WHEEL,sha256=osohxoshIHTFJFVPhsi1UkZuLRGMHRXZzwEBW2ezjrc,87
78
- langtrace_python_sdk-1.3.5.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
79
- langtrace_python_sdk-1.3.5.dist-info/RECORD,,
76
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
+ tests/utils.py,sha256=wdbR00LjYfDXzTBFvkksQYEz1hZjGTNKEiw5KPE_bqI,625
78
+ tests/anthropic/test_anthropic.py,sha256=vvrDJCg9KCws72NEuvPklq8RqQBgGwmV-fSkxGSvUFw,2632
79
+ tests/chroma/test_chroma.py,sha256=5zeInDcP5VplrM9ex2iuVKcpVKMDfEf_ZDK9D6Tc700,2392
80
+ tests/langchain/test_langchain.py,sha256=GGGRcxz0isNmegeum37XFrlJqI6jB6_iUvv8AJ5iG24,2481
81
+ tests/langchain/test_langchain_community.py,sha256=m9lBmMZIeUouKSq1JfdBupV0-0ef39GD6BKsA0Cf_08,2515
82
+ tests/langchain/test_langchain_core.py,sha256=hCuKkIUvDQOUBM2oEgMG3Iq_KNTwC2sH7_Y_IR5FIno,4238
83
+ tests/openai/conftest.py,sha256=r-Scvq1pP62gkvI4CC13nR19twlRQFUx8WuMe9qcesM,1138
84
+ tests/openai/test_chat_completion.py,sha256=iXz8RTU5oCfP2CWOhKJXlWwK-IfLubI8SW396sPxnV0,5310
85
+ tests/openai/test_embeddings.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
+ tests/openai/test_image_generation.py,sha256=tUyLTELi-nBOlp6yZ0hyPbLp04S_-qo-EtqAIJZeXuQ,3889
87
+ tests/openai/cassettes/test_async_chat_completion_streaming.yaml,sha256=0aZHFy9NvXegEDjGWyoG-_ItLr7JYAKbaBLIPSl-pfM,6844
88
+ tests/openai/cassettes/test_async_image_generation.yaml,sha256=_LYZcrqxrnSqcWVQn2Y0XMVGxF-wBrSAd-v3LTAIAeo,3597
89
+ tests/openai/cassettes/test_chat_completion.yaml,sha256=YkNFgK9VHAzNqGWuxFcTiE194GdEie8eDf1FSsffjd8,2944
90
+ tests/openai/cassettes/test_chat_completion_streaming.yaml,sha256=nkx_TemQMYSZxUF_b-LCEFwCRDm0AkQHLf4sdJVuZBw,2592394
91
+ tests/openai/cassettes/test_image_generation.yaml,sha256=gn5aSVp6V6_hb_rt2NnkAWd_idzDxo-7VzhZII0Wslw,3562
92
+ tests/pinecone/test_pinecone.py,sha256=_wlJbSKnY7gyzVcwxIWKft1P_t8dWwcIKNfGCrRLiHs,2633
93
+ langtrace_python_sdk-1.3.6.dist-info/METADATA,sha256=j3UsVVaot2VIVi5KP9cRUmUriQUP4WjuY1yFKEmJwtU,9244
94
+ langtrace_python_sdk-1.3.6.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
95
+ langtrace_python_sdk-1.3.6.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
96
+ langtrace_python_sdk-1.3.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.24.1
2
+ Generator: hatchling 1.24.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
tests/__init__.py ADDED
File without changes
@@ -0,0 +1,73 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, call
3
+ from langtrace_python_sdk.instrumentation.anthropic.patch import messages_create
4
+ from opentelemetry.trace import SpanKind
5
+ import importlib.metadata
6
+ from langtrace_python_sdk.constants.instrumentation.anthropic import APIS
7
+ from opentelemetry.trace.status import Status, StatusCode
8
+ import json
9
+ from langtrace.trace_attributes import Event, LLMSpanAttributes
10
+
11
+ from tests.utils import common_setup
12
+
13
+ class TestAnthropic(unittest.TestCase):
14
+
15
+ data = {
16
+ "content" : [MagicMock(text="Some text", type="text")],
17
+ "system_fingerprint" : "None",
18
+ "usage" : MagicMock(input_tokens=23, output_tokens=44),
19
+ "chunks" : [MagicMock(delta="Some text", message="text")]}
20
+
21
+
22
+ def setUp(self):
23
+
24
+ # Mock the original method
25
+ self.anthropic_mock, self.tracer, self.span = common_setup(self.data, None)
26
+
27
+ def tearDown(self):
28
+ pass
29
+
30
+ def test_anthropic(self):
31
+ # Arrange
32
+ version = importlib.metadata.version('anthropic')
33
+ kwargs = {
34
+ "model": "claude-3-opus-20240229",
35
+ "messages" : [{"role": "user", "content": "How are you today?"}],
36
+ "stream": False
37
+ }
38
+
39
+ # Act
40
+ wrapped_function = messages_create("anthropic.messages.create", version, self.tracer)
41
+ result = wrapped_function(self.anthropic_mock, MagicMock(), (), kwargs)
42
+
43
+
44
+ # Assert
45
+ self.assertTrue(self.tracer.start_as_current_span.called_once_with("anthropic.messages.create", kind=SpanKind.CLIENT))
46
+ self.assertTrue(self.span.set_status.has_calls([call(Status(StatusCode.OK))]))
47
+
48
+ expected_attributes = {
49
+ "langtrace.sdk.name": "langtrace-python-sdk",
50
+ "langtrace.service.name": "Anthropic",
51
+ "langtrace.service.type": "llm",
52
+ "langtrace.service.version": version,
53
+ "langtrace.version": "1.0.0",
54
+ "url.full": "/v1/messages",
55
+ "llm.api": APIS["MESSAGES_CREATE"]["ENDPOINT"],
56
+ "llm.model": kwargs.get("model"),
57
+ "llm.prompts": json.dumps(kwargs.get("messages", [])),
58
+ "llm.stream": kwargs.get("stream"),
59
+ }
60
+
61
+ self.assertTrue(
62
+ self.span.set_attribute.has_calls(
63
+ [call(key, value) for key, value in expected_attributes.items()], any_order=True
64
+ )
65
+ )
66
+
67
+ expected_result_data = {"system_fingerprint": "None" }
68
+
69
+ self.assertEqual(result.system_fingerprint, expected_result_data["system_fingerprint"])
70
+
71
+
72
+ if __name__ == '__main__':
73
+ unittest.main()
@@ -0,0 +1,64 @@
1
+ from unittest.mock import MagicMock, patch, call
2
+ from langtrace_python_sdk.constants.instrumentation.common import \
3
+ SERVICE_PROVIDERS
4
+ from langtrace_python_sdk.constants.instrumentation.openai import APIS
5
+ from opentelemetry.trace.status import StatusCode,Status
6
+ from langtrace_python_sdk.instrumentation.chroma.patch import collection_patch
7
+ from opentelemetry.trace import SpanKind
8
+ from tests.utils import common_setup
9
+ import unittest
10
+ import json
11
+
12
+
13
+ class TestChromaPatch(unittest.TestCase):
14
+ data = {
15
+ "status": "success",
16
+ }
17
+ def setUp(self):
18
+ self.chroma_mock, self.tracer, self.span = common_setup(self.data, 'chromadb.Collection.add')
19
+ self.wrapped_method = MagicMock(return_value="mocked method result")
20
+ self.instance = MagicMock()
21
+ self.instance.name = "aa"
22
+
23
+ def tearDown(self):
24
+ self.chroma_mock.stop()
25
+
26
+ def test_collection_patch_success(self):
27
+ # Arrange
28
+ traced_method = collection_patch("ADD", "1.2.3", self.tracer)
29
+
30
+ # Act
31
+ result = traced_method(self.wrapped_method, self.instance, (), {})
32
+
33
+ # Assert
34
+ # Assert the result of the original method is returned
35
+ self.assertEqual(result, "mocked method result")
36
+
37
+ # Assert the span is started with the correct parameters
38
+ self.assertTrue(self.tracer.start_as_current_span.called_once_with("chromadb.Collection.add", kind=SpanKind.CLIENT))
39
+
40
+ # Verify span attributes are set as expected
41
+ expected_attributes = {
42
+ 'langtrace.sdk.name': 'langtrace-python-sdk',
43
+ 'langtrace.service.name': 'Chroma',
44
+ 'langtrace.service.type': 'vectordb',
45
+ 'langtrace.service.version': '1.2.3',
46
+ 'langtrace.version': '1.0.0',
47
+ 'db.system': 'chromadb',
48
+ 'db.operation': 'add',
49
+ 'db.collection.name': 'aa',
50
+ }
51
+ for key, value in expected_attributes.items():
52
+ self.span.set_attribute.assert_has_calls([call(key, value)], any_order=True)
53
+
54
+ actual_calls = self.span.set_attribute.call_args_list
55
+
56
+ for key, value in expected_attributes.items():
57
+ self.assertIn(call(key, value), actual_calls)
58
+
59
+ # Assert the span status is set to OK
60
+ self.span.set_status.assert_called_with(StatusCode.OK)
61
+
62
+
63
+ if __name__ == '__main__':
64
+ unittest.main()
@@ -0,0 +1,69 @@
1
+
2
+ import unittest
3
+ from unittest.mock import MagicMock, call
4
+ from langtrace_python_sdk.instrumentation.langchain.patch import generic_patch
5
+ from opentelemetry.trace import SpanKind
6
+ from opentelemetry.trace import get_tracer
7
+ import importlib.metadata
8
+ from langtrace_python_sdk.constants.instrumentation.openai import APIS
9
+ from opentelemetry.trace.status import Status, StatusCode
10
+ from tests.utils import common_setup
11
+ import json
12
+
13
+ class TestGenericPatch(unittest.TestCase):
14
+ data = {"key": "value"}
15
+ def setUp(self):
16
+ self.langchain_mock, self.tracer, self.span = common_setup(self.data, None)
17
+
18
+ def tearDown(self):
19
+ # Clean up after each test case
20
+ pass
21
+
22
+ def test_generic_patch(self):
23
+ # Arrange
24
+ method_name = "example_method"
25
+ trace_output = False
26
+ trace_input = False # Change as per your requirement
27
+ args = (1, 2, 3)
28
+ task = "split_text"
29
+ kwargs = {'key': 'value'}
30
+ version = importlib.metadata.version('langchain')
31
+
32
+ # Act
33
+ wrapped_function = generic_patch("langchain.text_splitter", task, self.tracer, version, trace_output, trace_input)
34
+ result = wrapped_function(self.langchain_mock, MagicMock(), args, kwargs)
35
+
36
+ # Assert
37
+ self.assertTrue(self.tracer.start_as_current_span.called_once_with(method_name, kind=SpanKind.CLIENT))
38
+
39
+ service_provider = "Langchain"
40
+ expected_attributes = {
41
+ 'langtrace.sdk.name': 'langtrace-python-sdk',
42
+ "langtrace.service.name": service_provider,
43
+ "langtrace.service.type": "framework",
44
+ "langtrace.service.version": version,
45
+ "langtrace.version": "1.0.0",
46
+ "langchain.task.name": task,
47
+ }
48
+
49
+
50
+ self.assertTrue(
51
+ self.span.set_attribute.has_calls(
52
+ [call(key, value) for key, value in expected_attributes.items()], any_order=True
53
+ )
54
+ )
55
+
56
+ actual_calls = self.span.set_attribute.call_args_list
57
+
58
+ for key, value in expected_attributes.items():
59
+ self.assertIn(call(key, value), actual_calls)
60
+
61
+ self.assertEqual(self.span.set_status.call_count, 1)
62
+ self.assertTrue(self.span.set_status.has_calls([call(Status(StatusCode.OK))]))
63
+
64
+ expected_result_data = {"key": "value" }
65
+
66
+ self.assertEqual(result.key, expected_result_data["key"])
67
+
68
+ if __name__ == '__main__':
69
+ unittest.main()
@@ -0,0 +1,69 @@
1
+
2
+ import unittest
3
+ from unittest.mock import MagicMock, Mock, patch, call
4
+ from langtrace_python_sdk.instrumentation.langchain_community.patch import generic_patch
5
+ from opentelemetry.trace import SpanKind
6
+ from opentelemetry.trace import get_tracer
7
+ import importlib.metadata
8
+ import openai
9
+ from langtrace_python_sdk.constants.instrumentation.openai import APIS
10
+ from opentelemetry.trace.status import Status, StatusCode
11
+ import json
12
+ from tests.utils import common_setup
13
+ class TestGenericPatch(unittest.TestCase):
14
+ data = {"key": "value"}
15
+ def setUp(self):
16
+ self.langchain_mock, self.tracer, self.span = common_setup(self.data, None)
17
+
18
+ def tearDown(self):
19
+ # Clean up after each test case
20
+ pass
21
+
22
+ def test_generic_patch(self):
23
+ # Arrange
24
+ method_name = "example_method"
25
+ trace_output = False
26
+ trace_input = False
27
+ args = (1, 2, 3)
28
+ task = "vector_store"
29
+ kwargs = {'key': 'value'}
30
+ version = importlib.metadata.version("langchain-community")
31
+
32
+ # Act
33
+ wrapped_function = generic_patch("langchain_community.vectorstores.faiss", task, self.tracer, version, trace_output, trace_input)
34
+ result = wrapped_function(self.langchain_mock, MagicMock(), args, kwargs)
35
+
36
+ # Assert
37
+ self.assertTrue(self.tracer.start_as_current_span.called_once_with(method_name, kind=SpanKind.CLIENT))
38
+
39
+ service_provider = "Langchain Community"
40
+ expected_attributes = {
41
+ 'langtrace.sdk.name': 'langtrace-python-sdk',
42
+ "langtrace.service.name": service_provider,
43
+ "langtrace.service.type": "framework",
44
+ "langtrace.service.version": version,
45
+ "langtrace.version": "1.0.0",
46
+ "langchain.task.name": task,
47
+ }
48
+
49
+
50
+ self.assertTrue(
51
+ self.span.set_attribute.has_calls(
52
+ [call(key, value) for key, value in expected_attributes.items()], any_order=True
53
+ )
54
+ )
55
+
56
+ actual_calls = self.span.set_attribute.call_args_list
57
+
58
+ for key, value in expected_attributes.items():
59
+ self.assertIn(call(key, value), actual_calls)
60
+
61
+ self.assertEqual(self.span.set_status.call_count, 1)
62
+ self.assertTrue(self.span.set_status.has_calls([call(Status(StatusCode.OK))]))
63
+
64
+ expected_result_data = {"key": "value" }
65
+ self.assertEqual(result.key, expected_result_data["key"])
66
+
67
+
68
+ if __name__ == '__main__':
69
+ unittest.main()
@@ -0,0 +1,115 @@
1
+
2
+ import unittest
3
+ from unittest.mock import MagicMock, Mock, patch, call
4
+ from langtrace_python_sdk.instrumentation.langchain_core.patch import generic_patch, runnable_patch
5
+ from opentelemetry.trace import SpanKind
6
+ from opentelemetry.trace import get_tracer
7
+ import importlib.metadata
8
+ import openai
9
+ from langtrace_python_sdk.constants.instrumentation.openai import APIS
10
+ from opentelemetry.trace.status import Status, StatusCode
11
+ import json
12
+ from tests.utils import common_setup
13
+ class TestGenericPatch(unittest.TestCase):
14
+ data = {"items": "value"}
15
+ def setUp(self):
16
+ self.langchain_mock, self.tracer, self.span = common_setup(self.data, None)
17
+
18
+ def tearDown(self):
19
+ # Clean up after each test case
20
+ pass
21
+
22
+ def test_generic_patch(self):
23
+ # Arrange
24
+ method_name = "example_method"
25
+ trace_output = False
26
+ trace_input = True
27
+ task = "retriever"
28
+ args = (1, 2, 3)
29
+ kwargs = {'key': 'value'}
30
+ version = importlib.metadata.version("langchain-core")
31
+
32
+ # Act
33
+ wrapped_function = generic_patch("langchain_core.retrievers", task , self.tracer, version, trace_output, trace_input)
34
+ result = wrapped_function(self.langchain_mock, MagicMock(), args, kwargs)
35
+
36
+ # Assert
37
+ self.assertTrue(self.tracer.start_as_current_span.called_once_with(method_name, kind=SpanKind.CLIENT))
38
+
39
+ service_provider = "Langchain Core"
40
+ expected_attributes = {
41
+ 'langtrace.sdk.name': 'langtrace-python-sdk',
42
+ "langtrace.service.name": service_provider,
43
+ "langtrace.service.type": "framework",
44
+ "langtrace.service.version": version,
45
+ "langtrace.version": "1.0.0",
46
+ "langchain.task.name": task,
47
+ }
48
+
49
+ self.assertTrue(
50
+ self.span.set_attribute.has_calls(
51
+ [call(key, value) for key, value in expected_attributes.items()], any_order=True
52
+ )
53
+ )
54
+
55
+ actual_calls = self.span.set_attribute.call_args_list
56
+ for key, value in expected_attributes.items():
57
+ self.assertIn(call(key, value), actual_calls)
58
+
59
+
60
+ self.assertEqual(self.span.set_status.call_count, 1)
61
+ self.assertTrue(self.span.set_status.has_calls([call(Status(StatusCode.OK))]))
62
+
63
+ expected_result_data = {"items": "value" }
64
+ self.assertEqual(result.items, expected_result_data["items"])
65
+
66
+ def test_runnable_patch(self):
67
+ # Arrange
68
+ method_name = "example_method"
69
+ trace_output = False
70
+ trace_input = True
71
+ args = (1, 2, 3)
72
+ kwargs = {'key': 'value'}
73
+ version = importlib.metadata.version("langchain-core")
74
+
75
+ # Act
76
+ wrapped_function = runnable_patch("langchain_core.runnables.passthrough",
77
+ "runnablepassthrough", self.tracer, version, trace_output, trace_input)
78
+
79
+ result = wrapped_function(self.langchain_mock, MagicMock(), args, kwargs)
80
+
81
+ # Assert
82
+ self.assertTrue(self.tracer.start_as_current_span.called_once_with(method_name, kind=SpanKind.CLIENT))
83
+
84
+ service_provider = "Langchain Core"
85
+ expected_attributes = {
86
+ 'langtrace.sdk.name': 'langtrace-python-sdk',
87
+ "langtrace.service.name": service_provider,
88
+ "langtrace.service.type": "framework",
89
+ "langtrace.service.version": version,
90
+ "langtrace.version": "1.0.0",
91
+ "langchain.task.name": "runnablepassthrough",
92
+ }
93
+
94
+ self.assertTrue(
95
+ self.span.set_attribute.has_calls(
96
+ [call(key, value) for key, value in expected_attributes.items()], any_order=True
97
+ )
98
+ )
99
+
100
+ actual_calls = self.span.set_attribute.call_args_list
101
+
102
+ for key, value in expected_attributes.items():
103
+ self.assertIn(call(key, value), actual_calls)
104
+
105
+
106
+ self.assertEqual(self.span.set_status.call_count, 1)
107
+ self.assertTrue(self.span.set_status.has_calls([call(Status(StatusCode.OK))]))
108
+
109
+ expected_result_data = {"items": "value" }
110
+
111
+ self.assertEqual(result.items, expected_result_data["items"])
112
+
113
+
114
+ if __name__ == '__main__':
115
+ unittest.main()