turingpulse-sdk-mistral 1.0.0__tar.gz

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.
@@ -0,0 +1,42 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ .venv/
8
+ venv/
9
+ ENV/
10
+
11
+ # Distribution / packaging
12
+ dist/
13
+ build/
14
+ *.egg-info/
15
+
16
+ # Database files
17
+ *.db
18
+ *.sqlite3
19
+
20
+ # Environment variables
21
+ .env
22
+ .env.local
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+
30
+ # Testing
31
+ .pytest_cache/
32
+ .coverage
33
+ htmlcov/
34
+ .tox/
35
+
36
+ # Logs
37
+ *.log
38
+ logs/
39
+
40
+ # OS files
41
+ .DS_Store
42
+ Thumbs.db
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: turingpulse-sdk-mistral
3
+ Version: 1.0.0
4
+ Summary: TuringPulse SDK integration for Mistral AI
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: mistralai>=0.4.0
8
+ Requires-Dist: turingpulse-sdk>=1.0.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
11
+ Requires-Dist: pytest>=8.0; extra == 'dev'
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "turingpulse-sdk-mistral"
7
+ version = "1.0.0"
8
+ description = "TuringPulse SDK integration for Mistral AI"
9
+ requires-python = ">=3.11"
10
+ license = "Apache-2.0"
11
+ dependencies = [
12
+ "turingpulse-sdk>=1.0.0",
13
+ "mistralai>=0.4.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
@@ -0,0 +1,6 @@
1
+ """TuringPulse SDK integration for Mistral AI."""
2
+
3
+ from ._wrapper import patch_mistral, unpatch_mistral
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["patch_mistral", "unpatch_mistral"]
@@ -0,0 +1,147 @@
1
+ """Mistral AI monkey-patch instrumentation for TuringPulse."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from contextvars import ContextVar
8
+ from typing import Any, Optional
9
+
10
+ from turingpulse_sdk import instrument, GovernanceDirective
11
+ from turingpulse_sdk.config import MAX_FIELD_SIZE
12
+ from turingpulse_sdk.context import current_context
13
+ from turingpulse_sdk.exceptions import ConfigurationError
14
+
15
+ logger = logging.getLogger("turingpulse.sdk.mistral")
16
+
17
+ _INSTRUMENTING: ContextVar[bool] = ContextVar("_tp_mistral_instrumenting", default=False)
18
+
19
+ _ORIGINAL_COMPLETE: Any = None
20
+
21
+
22
+ def patch_mistral(
23
+ *,
24
+ name: str | None = None,
25
+ governance: Optional[GovernanceDirective] = None,
26
+ ) -> None:
27
+ """Monkey-patch ``mistralai.Mistral.chat.complete``."""
28
+ global _ORIGINAL_COMPLETE
29
+
30
+ effective_name = name or os.getenv("TP_WORKFLOW_NAME", "")
31
+ if not effective_name:
32
+ raise ConfigurationError(
33
+ "patch_mistral() requires name= or TP_WORKFLOW_NAME to be set."
34
+ )
35
+
36
+ try:
37
+ from mistralai import Mistral
38
+ except ImportError as exc:
39
+ raise ImportError("mistralai package is required: pip install mistralai>=0.4.0") from exc
40
+
41
+ if _ORIGINAL_COMPLETE is not None:
42
+ logger.warning("Mistral is already patched — skipping")
43
+ return
44
+
45
+ chat_cls = getattr(Mistral, "chat", None)
46
+ if chat_cls is None:
47
+ try:
48
+ from mistralai.chat import Chat
49
+ chat_cls = Chat
50
+ except ImportError:
51
+ logger.warning(
52
+ "Cannot locate Mistral.chat or Chat class — Mistral SDK version may be incompatible. Skipping patch."
53
+ )
54
+ return
55
+
56
+ _ORIGINAL_COMPLETE = getattr(chat_cls, "complete", None)
57
+ if _ORIGINAL_COMPLETE is None or not callable(_ORIGINAL_COMPLETE):
58
+ _ORIGINAL_COMPLETE = None
59
+ logger.warning(
60
+ "Cannot locate Chat.complete — Mistral SDK version may be incompatible. Skipping patch."
61
+ )
62
+ return
63
+
64
+ @instrument(name=effective_name, governance=governance)
65
+ def _patched_complete(self_chat, *args: Any, **kwargs: Any) -> Any:
66
+ if _INSTRUMENTING.get(False):
67
+ return _ORIGINAL_COMPLETE(self_chat, *args, **kwargs)
68
+ token = _INSTRUMENTING.set(True)
69
+ try:
70
+ response = _ORIGINAL_COMPLETE(self_chat, *args, **kwargs)
71
+ _record_mistral_span(response, kwargs)
72
+ return response
73
+ finally:
74
+ _INSTRUMENTING.reset(token)
75
+
76
+ try:
77
+ chat_cls.complete = _patched_complete
78
+ except (AttributeError, TypeError) as exc:
79
+ _ORIGINAL_COMPLETE = None
80
+ logger.warning("Failed to patch Chat.complete: %s", type(exc).__name__)
81
+ return
82
+ logger.info("Mistral patched for TuringPulse instrumentation")
83
+
84
+
85
+ def unpatch_mistral() -> None:
86
+ """Restore original Mistral methods."""
87
+ global _ORIGINAL_COMPLETE
88
+ if _ORIGINAL_COMPLETE is None:
89
+ return
90
+ try:
91
+ from mistralai.chat import Chat
92
+ Chat.complete = _ORIGINAL_COMPLETE
93
+ except ImportError:
94
+ try:
95
+ from mistralai import Mistral
96
+ if hasattr(Mistral, "chat"):
97
+ Mistral.chat.complete = _ORIGINAL_COMPLETE
98
+ except ImportError:
99
+ pass
100
+ _ORIGINAL_COMPLETE = None
101
+ logger.info("Mistral unpatched — original methods restored")
102
+
103
+
104
+ def _record_mistral_span(response: Any, kwargs: dict) -> None:
105
+ ctx = current_context()
106
+ if not ctx:
107
+ return
108
+ ctx.framework = "mistral"
109
+ ctx.node_type = "llm"
110
+
111
+ usage = getattr(response, "usage", None)
112
+ if usage:
113
+ ctx.set_tokens(
114
+ getattr(usage, "prompt_tokens", 0) or 0,
115
+ getattr(usage, "completion_tokens", 0) or 0,
116
+ )
117
+
118
+ ctx.set_model(kwargs.get("model", getattr(response, "model", "unknown")), "mistral")
119
+
120
+ messages = kwargs.get("messages", [])
121
+ if messages:
122
+ last_user = next(
123
+ (m for m in reversed(messages) if (m.get("role") if isinstance(m, dict) else getattr(m, "role", "")) == "user"),
124
+ None,
125
+ )
126
+ if last_user:
127
+ content = last_user.get("content", "") if isinstance(last_user, dict) else getattr(last_user, "content", "")
128
+ ctx.set_prompt(str(content)[:MAX_FIELD_SIZE])
129
+
130
+ choices = getattr(response, "choices", [])
131
+ if choices:
132
+ message = getattr(choices[0], "message", None)
133
+ if message:
134
+ content = getattr(message, "content", "")
135
+ if content:
136
+ ctx.set_io(output_data=str(content)[:MAX_FIELD_SIZE])
137
+
138
+ tool_calls = getattr(message, "tool_calls", None)
139
+ if tool_calls:
140
+ for tc in tool_calls:
141
+ func = getattr(tc, "function", None)
142
+ if func:
143
+ ctx.add_tool_call(
144
+ tool_name=getattr(func, "name", "unknown"),
145
+ tool_args={"arguments": getattr(func, "arguments", "")},
146
+ tool_id=getattr(tc, "id", None),
147
+ )