chatmodeltask 0.1.1__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.
- chatmodeltask-0.1.1/LICENSE +10 -0
- chatmodeltask-0.1.1/PKG-INFO +38 -0
- chatmodeltask-0.1.1/README.md +22 -0
- chatmodeltask-0.1.1/chatModelTask/__init__.py +9 -0
- chatmodeltask-0.1.1/chatModelTask/chatModelTask.py +152 -0
- chatmodeltask-0.1.1/chatModelTask/cli.py +9 -0
- chatmodeltask-0.1.1/chatModelTask/client.py +58 -0
- chatmodeltask-0.1.1/chatModelTask/config.py +9 -0
- chatmodeltask-0.1.1/chatModelTask/env.py +24 -0
- chatmodeltask-0.1.1/chatModelTask/model_store.py +131 -0
- chatmodeltask-0.1.1/chatModelTask/model_weight.py +82 -0
- chatmodeltask-0.1.1/chatModelTask/type.py +53 -0
- chatmodeltask-0.1.1/chatModelTask/utils.py +22 -0
- chatmodeltask-0.1.1/chatmodeltask.egg-info/PKG-INFO +38 -0
- chatmodeltask-0.1.1/chatmodeltask.egg-info/SOURCES.txt +18 -0
- chatmodeltask-0.1.1/chatmodeltask.egg-info/dependency_links.txt +1 -0
- chatmodeltask-0.1.1/chatmodeltask.egg-info/requires.txt +6 -0
- chatmodeltask-0.1.1/chatmodeltask.egg-info/top_level.txt +1 -0
- chatmodeltask-0.1.1/pyproject.toml +33 -0
- chatmodeltask-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatmodeltask
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Smart model routing layer for LangChain + OpenRouter
|
|
5
|
+
Author-email: MO7AMED DEV <programmingi77i@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: redis<6,>=5
|
|
10
|
+
Requires-Dist: httpx<1,>=0.27
|
|
11
|
+
Requires-Dist: langchain<2,>=1.3
|
|
12
|
+
Requires-Dist: langchain-openrouter<1,>=0.2
|
|
13
|
+
Requires-Dist: python-dotenv<2,>=1
|
|
14
|
+
Requires-Dist: thefuzz<1,>=0.22
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# ChatModelTask
|
|
18
|
+
|
|
19
|
+
Smart model routing layer for LangChain + OpenRouter.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install chatmodeltask
|
|
25
|
+
```
|
|
26
|
+
```python
|
|
27
|
+
from chatModelTask import ChatModelTask
|
|
28
|
+
|
|
29
|
+
llm = ChatModelTask(task_description="summarization")
|
|
30
|
+
print(llm.invoke("Hello world"))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Environment Variables
|
|
34
|
+
|
|
35
|
+
OPENROUTER_API_KEY=...
|
|
36
|
+
REDIS_HOST=localhost
|
|
37
|
+
MODEL_SCORE_ENGINE_BASE_URL=...
|
|
38
|
+
MODEL_SCORE_ENGINE_API_KEY=...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ChatModelTask
|
|
2
|
+
|
|
3
|
+
Smart model routing layer for LangChain + OpenRouter.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install chatmodeltask
|
|
9
|
+
```
|
|
10
|
+
```python
|
|
11
|
+
from chatModelTask import ChatModelTask
|
|
12
|
+
|
|
13
|
+
llm = ChatModelTask(task_description="summarization")
|
|
14
|
+
print(llm.invoke("Hello world"))
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Environment Variables
|
|
18
|
+
|
|
19
|
+
OPENROUTER_API_KEY=...
|
|
20
|
+
REDIS_HOST=localhost
|
|
21
|
+
MODEL_SCORE_ENGINE_BASE_URL=...
|
|
22
|
+
MODEL_SCORE_ENGINE_API_KEY=...
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Any, get_args
|
|
2
|
+
from .model_store import ModelCache
|
|
3
|
+
|
|
4
|
+
from langchain_openrouter import ChatOpenRouter
|
|
5
|
+
from .type import ModelFilter, TASK_TYPE
|
|
6
|
+
from .client import ChatModelTaskClient
|
|
7
|
+
from .model_weight import generateTaskWeights
|
|
8
|
+
from .utils import hash_dict
|
|
9
|
+
from .config import settings
|
|
10
|
+
|
|
11
|
+
#def secret_from_env(key: str) -> Optional[str]:
|
|
12
|
+
# return os.getenv(key)
|
|
13
|
+
|
|
14
|
+
class ChatModelTask(ChatOpenRouter):
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
task: Optional[TASK_TYPE] = None,
|
|
19
|
+
task_description: Optional[str] = None,
|
|
20
|
+
task_weights: Optional[dict] = None,
|
|
21
|
+
model: Optional[str] = None,
|
|
22
|
+
filter: Optional[ModelFilter] = None,
|
|
23
|
+
top_k: Optional[int] = 10,
|
|
24
|
+
name: Optional[str] = None,
|
|
25
|
+
base_url: Optional[str] = None,
|
|
26
|
+
api_key: Optional[str] = None,
|
|
27
|
+
openRouter_api_key: Optional[str] = None,
|
|
28
|
+
temperature: Optional[float] = None,
|
|
29
|
+
max_tokens: Optional[int] = None,
|
|
30
|
+
max_completion_tokens: Optional[int] = None,
|
|
31
|
+
timeout: int = 300,
|
|
32
|
+
max_retries: int = 2,
|
|
33
|
+
streaming: bool = False,
|
|
34
|
+
stream_usage: bool = True,
|
|
35
|
+
tags: Optional[List[str]] = None,
|
|
36
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
37
|
+
|
|
38
|
+
**kwargs: Any
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Custom ChatModelTask SDK for OpenRouter.
|
|
42
|
+
|
|
43
|
+
Environment Variables Required (if not passed explicitly):
|
|
44
|
+
MODEL_SCORE_ENGINE_BASE_URL: The base URL for the model score engine API.
|
|
45
|
+
MODEL_SCORE_ENGINE_API_KEY: The API secret key for authentication.
|
|
46
|
+
OPENROUTER_API_KEY: the API key for openrouter
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
base_url = base_url or settings.MODEL_SCORE_ENGINE_BASE_URL
|
|
52
|
+
api_key = api_key or settings.MODEL_SCORE_ENGINE_API_KEY
|
|
53
|
+
openRouter_api_key = openRouter_api_key or settings.OPENROUTER_API_KEY
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if not base_url:
|
|
57
|
+
raise ValueError("MODEL_TASK_BASE_URL is required")
|
|
58
|
+
if not api_key:
|
|
59
|
+
raise ValueError("MODEL_TASK_API_KEY is required")
|
|
60
|
+
|
|
61
|
+
task_description = task if not task_description else task_description
|
|
62
|
+
|
|
63
|
+
payload = {
|
|
64
|
+
"task": task,
|
|
65
|
+
"weights": task_weights if task_weights else None,
|
|
66
|
+
"filter": filter.model_dump() if filter else None,
|
|
67
|
+
"top_k": top_k
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
models = None
|
|
73
|
+
if(task_description and not model and not task_weights):
|
|
74
|
+
|
|
75
|
+
model_cache = ModelCache() if task_description else None
|
|
76
|
+
hashFilter = hash_dict(filter.model_dump()) if filter else ''
|
|
77
|
+
|
|
78
|
+
models = model_cache.get_models(
|
|
79
|
+
task_description,
|
|
80
|
+
exp_seconds=3600,
|
|
81
|
+
refresh_func=lambda weights=None:self._refresh_models(weights,task_description,hashFilter,payload,base_url,api_key,timeout),
|
|
82
|
+
hashFilter=hashFilter)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if not models:
|
|
86
|
+
if not model:
|
|
87
|
+
client = ChatModelTaskClient(base_url, api_key, timeout)
|
|
88
|
+
models = client.post_sync("/rank/models", payload)
|
|
89
|
+
else:
|
|
90
|
+
models = [{"id": model}]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
super().__init__(
|
|
94
|
+
model=models[0].get('id'),
|
|
95
|
+
temperature=temperature,
|
|
96
|
+
max_tokens=max_tokens,
|
|
97
|
+
max_completion_tokens=max_completion_tokens,
|
|
98
|
+
api_key=openRouter_api_key,
|
|
99
|
+
max_retries=max_retries,
|
|
100
|
+
streaming=streaming,
|
|
101
|
+
tags=tags,
|
|
102
|
+
metadata=metadata,
|
|
103
|
+
stream_usage=stream_usage,
|
|
104
|
+
name=name,
|
|
105
|
+
**kwargs
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# FIXED: Use object.__setattr__ to bypass Pydantic validation rules safely
|
|
110
|
+
object.__setattr__(self, 'models', models)
|
|
111
|
+
# Store openRouter_api_key internally so fallbacks can access it if needed
|
|
112
|
+
object.__setattr__(self, '_openrouter_api_key', openRouter_api_key)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _refresh_models(self,weights,task_description,hashFilter,payload,base_url, api_key, timeout):
|
|
117
|
+
|
|
118
|
+
isTaskType = task_description in get_args(TASK_TYPE)
|
|
119
|
+
|
|
120
|
+
weights = generateTaskWeights(task_description) if not weights and not isTaskType else weights
|
|
121
|
+
payload['weights'] = weights or {}
|
|
122
|
+
|
|
123
|
+
client = ChatModelTaskClient(base_url, api_key, timeout)
|
|
124
|
+
models = client.post_sync("/rank/models", payload)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"models":[m.get('id') for m in models],
|
|
128
|
+
"weights":weights,
|
|
129
|
+
"hashFilter":hashFilter
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def with_fallbacks(self, fallbacks: Optional[List[str]] = None, **kwargs: Any):
|
|
134
|
+
fallbacks = fallbacks if fallbacks else [m.get('id') for i, m in enumerate(self.models) if i > 0]
|
|
135
|
+
|
|
136
|
+
# Extract the saved key to ensure fallback instances can authenticate properly
|
|
137
|
+
api_key = getattr(self, '_openrouter_api_key', None)
|
|
138
|
+
|
|
139
|
+
return super().with_fallbacks(
|
|
140
|
+
[
|
|
141
|
+
ChatOpenRouter(
|
|
142
|
+
model=modelID,
|
|
143
|
+
api_key=api_key,
|
|
144
|
+
**kwargs
|
|
145
|
+
) for modelID in fallbacks
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChatModelTaskClient:
|
|
5
|
+
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
base_url: str,
|
|
9
|
+
api_key: str,
|
|
10
|
+
timeout: int = 300
|
|
11
|
+
):
|
|
12
|
+
self.base_url = base_url.rstrip("/")
|
|
13
|
+
self.api_key = api_key
|
|
14
|
+
self.timeout = timeout
|
|
15
|
+
|
|
16
|
+
# -------------------------
|
|
17
|
+
# ASYNC version
|
|
18
|
+
# -------------------------
|
|
19
|
+
async def post(self, endpoint: str, json: dict):
|
|
20
|
+
|
|
21
|
+
headers = {
|
|
22
|
+
"Authorization": f"Bearer {self.api_key}"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async with httpx.AsyncClient(
|
|
26
|
+
timeout=self.timeout
|
|
27
|
+
) as client:
|
|
28
|
+
|
|
29
|
+
response = await client.post(
|
|
30
|
+
f"{self.base_url}{endpoint}",
|
|
31
|
+
json=json,
|
|
32
|
+
headers=headers
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
return response.json()
|
|
37
|
+
|
|
38
|
+
# -------------------------
|
|
39
|
+
# SYNC version
|
|
40
|
+
# -------------------------
|
|
41
|
+
def post_sync(self, endpoint: str, json: dict):
|
|
42
|
+
|
|
43
|
+
headers = {
|
|
44
|
+
"Authorization": f"Bearer {self.api_key}"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
with httpx.Client(
|
|
48
|
+
timeout=self.timeout
|
|
49
|
+
) as client:
|
|
50
|
+
|
|
51
|
+
response = client.post(
|
|
52
|
+
f"{self.base_url}{endpoint}",
|
|
53
|
+
json=json,
|
|
54
|
+
headers=headers
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
return response.json()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
class Settings:
|
|
4
|
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
|
5
|
+
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
|
|
6
|
+
MODEL_SCORE_ENGINE_BASE_URL = os.getenv("MODEL_SCORE_ENGINE_BASE_URL")
|
|
7
|
+
MODEL_SCORE_ENGINE_API_KEY = os.getenv("MODEL_SCORE_ENGINE_API_KEY")
|
|
8
|
+
|
|
9
|
+
settings = Settings()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def init_env(path: str = ".env"):
|
|
5
|
+
"""
|
|
6
|
+
Create .env file from bundled example
|
|
7
|
+
"""
|
|
8
|
+
example_path = Path(__file__).parent / ".env.example"
|
|
9
|
+
|
|
10
|
+
if not example_path.exists():
|
|
11
|
+
raise FileNotFoundError("Missing .env.example in package")
|
|
12
|
+
|
|
13
|
+
if Path(path).exists():
|
|
14
|
+
print(f"{path} already exists")
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
content = example_path.read_text()
|
|
18
|
+
Path(path).write_text(content)
|
|
19
|
+
|
|
20
|
+
print(f"{path} created from template")
|
|
21
|
+
|
|
22
|
+
def load_env():
|
|
23
|
+
from dotenv import load_dotenv
|
|
24
|
+
load_dotenv()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import redis
|
|
2
|
+
import json
|
|
3
|
+
import hashlib
|
|
4
|
+
import time
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
r = redis.Redis(
|
|
10
|
+
host=os.getenv('REDIS_HOST'),
|
|
11
|
+
port=6379,
|
|
12
|
+
decode_responses=True
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ModelCache:
|
|
17
|
+
|
|
18
|
+
# 30 days
|
|
19
|
+
IDLE_EXPIRE_SECONDS = 60 * 60 * 24 * 30
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.r = r
|
|
23
|
+
|
|
24
|
+
# -----------------------------
|
|
25
|
+
# key generator
|
|
26
|
+
# -----------------------------
|
|
27
|
+
def make_key(self, task_description: str) -> str:
|
|
28
|
+
hash_value = hashlib.sha256(
|
|
29
|
+
task_description.strip().lower().encode()
|
|
30
|
+
).hexdigest()
|
|
31
|
+
|
|
32
|
+
return f"model_cache:{hash_value}"
|
|
33
|
+
|
|
34
|
+
# -----------------------------
|
|
35
|
+
# SET
|
|
36
|
+
# -----------------------------
|
|
37
|
+
def set_models(
|
|
38
|
+
self,
|
|
39
|
+
text: str,
|
|
40
|
+
models: List[str],
|
|
41
|
+
weights: dict,
|
|
42
|
+
hashFilter: str = ''
|
|
43
|
+
):
|
|
44
|
+
|
|
45
|
+
key = self.make_key(text)
|
|
46
|
+
|
|
47
|
+
value = {
|
|
48
|
+
"models": models,
|
|
49
|
+
"weights": weights,
|
|
50
|
+
"updated_at": time.time(),
|
|
51
|
+
"hashFilter": hashFilter
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# set with TTL
|
|
55
|
+
self.r.set(
|
|
56
|
+
key,
|
|
57
|
+
json.dumps(value),
|
|
58
|
+
ex=self.IDLE_EXPIRE_SECONDS
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# -----------------------------
|
|
62
|
+
# CHECK IF EXPIRED
|
|
63
|
+
# -----------------------------
|
|
64
|
+
def is_expired(self, data: dict, exp_seconds: int) -> bool:
|
|
65
|
+
return (time.time() - data["updated_at"]) > exp_seconds
|
|
66
|
+
|
|
67
|
+
# -----------------------------
|
|
68
|
+
# GET
|
|
69
|
+
# -----------------------------
|
|
70
|
+
def get_models(
|
|
71
|
+
self,
|
|
72
|
+
text: str,
|
|
73
|
+
exp_seconds: int = 3600,
|
|
74
|
+
refresh_func=None,
|
|
75
|
+
hashFilter: Optional[str] = '',
|
|
76
|
+
) -> Optional[dict]:
|
|
77
|
+
|
|
78
|
+
key = self.make_key(text)
|
|
79
|
+
|
|
80
|
+
data = self.r.get(key)
|
|
81
|
+
|
|
82
|
+
# -----------------
|
|
83
|
+
# cache miss
|
|
84
|
+
# -----------------
|
|
85
|
+
if not data:
|
|
86
|
+
if refresh_func:
|
|
87
|
+
new_data = refresh_func()
|
|
88
|
+
|
|
89
|
+
self.set_models(
|
|
90
|
+
text,
|
|
91
|
+
new_data["models"],
|
|
92
|
+
new_data["weights"],
|
|
93
|
+
new_data["hashFilter"]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return [{"id": m} for m in new_data['models']]
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# refresh idle TTL on access
|
|
101
|
+
self.r.expire(key, self.IDLE_EXPIRE_SECONDS)
|
|
102
|
+
|
|
103
|
+
data = json.loads(data)
|
|
104
|
+
|
|
105
|
+
hashed_filter = data['hashFilter']
|
|
106
|
+
|
|
107
|
+
# -----------------
|
|
108
|
+
# fresh cache
|
|
109
|
+
# -----------------
|
|
110
|
+
if (
|
|
111
|
+
not self.is_expired(data, exp_seconds)
|
|
112
|
+
and hashed_filter == hashFilter
|
|
113
|
+
):
|
|
114
|
+
return [{"id": m} for m in data['models']]
|
|
115
|
+
|
|
116
|
+
# -----------------
|
|
117
|
+
# expired → refresh
|
|
118
|
+
# -----------------
|
|
119
|
+
if refresh_func:
|
|
120
|
+
new_data = refresh_func(data['weights'])
|
|
121
|
+
|
|
122
|
+
self.set_models(
|
|
123
|
+
text,
|
|
124
|
+
new_data["models"],
|
|
125
|
+
new_data["weights"],
|
|
126
|
+
new_data["hashFilter"]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return [{"id": m} for m in new_data['models']]
|
|
130
|
+
|
|
131
|
+
return [{"id": m} for m in data['models']]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from chatModelTask.type import CustomWeights
|
|
2
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
weight_data = {
|
|
6
|
+
"popularity": 0.0,
|
|
7
|
+
"reasoning": 0.4,
|
|
8
|
+
"tools": 0.0,
|
|
9
|
+
"reliability": 0.3,
|
|
10
|
+
"cache": 0.0,
|
|
11
|
+
"programming": 0.2,
|
|
12
|
+
"science": 0.0,
|
|
13
|
+
"technology": 0.1,
|
|
14
|
+
"finance": 0.0,
|
|
15
|
+
"marketing": 0.0,
|
|
16
|
+
"translation": 0.0,
|
|
17
|
+
"legal": 0.0,
|
|
18
|
+
"health": 0.0,
|
|
19
|
+
"roleplay": 0.0,
|
|
20
|
+
"academia": 0.0,
|
|
21
|
+
"marketing_seo": 0.0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
system_instruction = """
|
|
26
|
+
You are an expert AI Systems Architect specializing in LLM routing and evaluation.
|
|
27
|
+
Your job is to translate a user's natural language request into a precise dictionary of feature weights for a recommendation engine.
|
|
28
|
+
|
|
29
|
+
Available features you can assign weights to:
|
|
30
|
+
- popularity (Overall consumption)
|
|
31
|
+
- reasoning (Complex logic, math, chain-of-thought)
|
|
32
|
+
- tools (Function calling/API execution)
|
|
33
|
+
- reliability (Low error rates in production)
|
|
34
|
+
- cache (Cost optimization/prompt caching)
|
|
35
|
+
- programming (Coding, debugging, script generation)
|
|
36
|
+
- science (Hard sciences, chemistry, physics)
|
|
37
|
+
- technology (IT systems, hardware, generic engineering)
|
|
38
|
+
- finance (Economic analysis, numbers, spreadsheets)
|
|
39
|
+
- marketing (Content creation, ad copywriting)
|
|
40
|
+
- translation (Language accuracy)
|
|
41
|
+
- legal (Law, compliance, text dense evaluation)
|
|
42
|
+
- health (Medicine, wellness data)
|
|
43
|
+
- roleplay (Chat personas, fiction writing)
|
|
44
|
+
- academia (Research papers, thesis styling)
|
|
45
|
+
- marketing/seo (SEO metadata, web positioning)
|
|
46
|
+
|
|
47
|
+
CRITICAL RULES:
|
|
48
|
+
1. Identify the core intent of the user request and allocate weights only to the most relevant features.
|
|
49
|
+
2. The SUM of all weights you assign MUST equal exactly 1.0 (or 100%).
|
|
50
|
+
3. If a feature is completely irrelevant to the user's task, leave it as 0.0.
|
|
51
|
+
4. Distribute the 1.0 budget intelligently based on the priorities implicit in the user text.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def generateTaskWeights(task_description: str) -> CustomWeights:
|
|
55
|
+
from chatModelTask.chatModelTask import ChatModelTask,ModelFilter
|
|
56
|
+
|
|
57
|
+
filter = ModelFilter(max_1m_input_tokens_price=0.1,
|
|
58
|
+
max_1m_output_tokens_price=0.5,
|
|
59
|
+
require_structured=True,
|
|
60
|
+
exclude_free_models=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
llm = ChatModelTask( task_weights=weight_data,
|
|
64
|
+
filter=filter,
|
|
65
|
+
base_url="http://localhost:8000",
|
|
66
|
+
api_key="123456789",
|
|
67
|
+
temperature=0.0)
|
|
68
|
+
|
|
69
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
70
|
+
("system", system_instruction),
|
|
71
|
+
("human", "Analyze this request and generate the weights: '{task_description}'")
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
# Bind the Pydantic schema to force structured JSON output
|
|
75
|
+
structured_llm = llm.with_structured_output(CustomWeights)
|
|
76
|
+
|
|
77
|
+
# Combine into a runnable chain
|
|
78
|
+
weight_generation_chain = prompt | structured_llm
|
|
79
|
+
|
|
80
|
+
weights = weight_generation_chain.invoke({"task_description":task_description})
|
|
81
|
+
|
|
82
|
+
return weights.model_dump() if weights else None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from pydantic import BaseModel,Field
|
|
2
|
+
from typing import Optional,Literal,List
|
|
3
|
+
|
|
4
|
+
MODALITY = Literal['text','image','video','file','audio']
|
|
5
|
+
|
|
6
|
+
class CustomWeights(BaseModel):
|
|
7
|
+
# Global metrics
|
|
8
|
+
popularity: Optional[float] = Field(default=0.0, description="Weight for overall model usage volume.")
|
|
9
|
+
reasoning: Optional[float] = Field(default=0.0, description="Weight for deep thinking, complex logic, and chain-of-thought capabilities.")
|
|
10
|
+
tools: Optional[float] = Field(default=0.0, description="Weight for function calling and external tool usage capabilities.")
|
|
11
|
+
reliability: Optional[float] = Field(default=0.0, description="Weight for low error rates during structured execution.")
|
|
12
|
+
cache: Optional[float] = Field(default=0.0, description="Weight for prompt caching efficiency and cost savings.")
|
|
13
|
+
|
|
14
|
+
# Category metrics
|
|
15
|
+
programming: Optional[float] = Field(default=0.0, description="Weight for software engineering, code generation, and debugging.")
|
|
16
|
+
science: Optional[float] = Field(default=0.0, description="Weight for scientific research, physics, chemistry, etc.")
|
|
17
|
+
technology: Optional[float] = Field(default=0.0, description="Weight for technical, IT, and general engineering topics.")
|
|
18
|
+
finance: Optional[float] = Field(default=0.0, description="Weight for financial analysis, math, spreadsheet logic, and market data.")
|
|
19
|
+
marketing: Optional[float] = Field(default=0.0, description="Weight for copy-writing, creative content generation, and ad hooks.")
|
|
20
|
+
translation: Optional[float] = Field(default=0.0, description="Weight for multi-language translation and localization accuracy.")
|
|
21
|
+
legal: Optional[float] = Field(default=0.0, description="Weight for contract analysis, legal terminology, and compliance.")
|
|
22
|
+
health: Optional[float] = Field(default=0.0, description="Weight for medical, biological, and health-related general information.")
|
|
23
|
+
roleplay: Optional[float] = Field(default=0.0, description="Weight for conversational depth, character persona maintenance, and creative writing.")
|
|
24
|
+
academia: Optional[float] = Field(default=0.0, description="Weight for academic writing, citations, and research papers.")
|
|
25
|
+
|
|
26
|
+
marketing_seo: Optional[float] = Field(default=0.0, alias="marketing/seo", description="Weight specifically for search engine optimization strategy and web ranking copy.")
|
|
27
|
+
|
|
28
|
+
model_config = {
|
|
29
|
+
"populate_by_name": True
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class ModelFilter(BaseModel):
|
|
33
|
+
max_1m_input_tokens_price: Optional[float] = 1
|
|
34
|
+
max_1m_output_tokens_price: Optional[float] = 2
|
|
35
|
+
|
|
36
|
+
require_tools: Optional[bool] = None
|
|
37
|
+
require_structured: Optional[bool] = None
|
|
38
|
+
|
|
39
|
+
min_context_length: Optional[int] = None
|
|
40
|
+
|
|
41
|
+
exclude_free_models:Optional[bool] = False
|
|
42
|
+
|
|
43
|
+
input_modality:Optional[List[MODALITY]]=['text']
|
|
44
|
+
output_modality:Optional[List[MODALITY]]=['text']
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
TASK_TYPE= Literal['programming',
|
|
49
|
+
'roleplay',
|
|
50
|
+
'marketing','marketing_seo',
|
|
51
|
+
'technology','science',
|
|
52
|
+
'translation','legal','finance',
|
|
53
|
+
'health','trivia','academia']
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def hash_string(text: str) -> str:
|
|
6
|
+
|
|
7
|
+
hash_value = hashlib.sha256(
|
|
8
|
+
text.strip().lower().encode()
|
|
9
|
+
).hexdigest()
|
|
10
|
+
|
|
11
|
+
return hash_value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def hash_dict(obj: dict) -> str:
|
|
15
|
+
|
|
16
|
+
text = json.dumps(
|
|
17
|
+
obj,
|
|
18
|
+
sort_keys=True,
|
|
19
|
+
separators=(",", ":")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return hash_string(text)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatmodeltask
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Smart model routing layer for LangChain + OpenRouter
|
|
5
|
+
Author-email: MO7AMED DEV <programmingi77i@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: redis<6,>=5
|
|
10
|
+
Requires-Dist: httpx<1,>=0.27
|
|
11
|
+
Requires-Dist: langchain<2,>=1.3
|
|
12
|
+
Requires-Dist: langchain-openrouter<1,>=0.2
|
|
13
|
+
Requires-Dist: python-dotenv<2,>=1
|
|
14
|
+
Requires-Dist: thefuzz<1,>=0.22
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# ChatModelTask
|
|
18
|
+
|
|
19
|
+
Smart model routing layer for LangChain + OpenRouter.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install chatmodeltask
|
|
25
|
+
```
|
|
26
|
+
```python
|
|
27
|
+
from chatModelTask import ChatModelTask
|
|
28
|
+
|
|
29
|
+
llm = ChatModelTask(task_description="summarization")
|
|
30
|
+
print(llm.invoke("Hello world"))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Environment Variables
|
|
34
|
+
|
|
35
|
+
OPENROUTER_API_KEY=...
|
|
36
|
+
REDIS_HOST=localhost
|
|
37
|
+
MODEL_SCORE_ENGINE_BASE_URL=...
|
|
38
|
+
MODEL_SCORE_ENGINE_API_KEY=...
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
chatModelTask/__init__.py
|
|
5
|
+
chatModelTask/chatModelTask.py
|
|
6
|
+
chatModelTask/cli.py
|
|
7
|
+
chatModelTask/client.py
|
|
8
|
+
chatModelTask/config.py
|
|
9
|
+
chatModelTask/env.py
|
|
10
|
+
chatModelTask/model_store.py
|
|
11
|
+
chatModelTask/model_weight.py
|
|
12
|
+
chatModelTask/type.py
|
|
13
|
+
chatModelTask/utils.py
|
|
14
|
+
chatmodeltask.egg-info/PKG-INFO
|
|
15
|
+
chatmodeltask.egg-info/SOURCES.txt
|
|
16
|
+
chatmodeltask.egg-info/dependency_links.txt
|
|
17
|
+
chatmodeltask.egg-info/requires.txt
|
|
18
|
+
chatmodeltask.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chatModelTask
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=70", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chatmodeltask"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Smart model routing layer for LangChain + OpenRouter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
|
|
12
|
+
authors = [
|
|
13
|
+
{ name="MO7AMED DEV", email="programmingi77i@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"redis>=5,<6",
|
|
18
|
+
"httpx>=0.27,<1",
|
|
19
|
+
"langchain>=1.3,<2",
|
|
20
|
+
"langchain-openrouter>=0.2,<1",
|
|
21
|
+
"python-dotenv>=1,<2",
|
|
22
|
+
"thefuzz>=0.22,<1"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
include-package-data = true
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["chatModelTask*"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.package-data]
|
|
33
|
+
chatModelTask = ["*.env.example"]
|