File size: 4,576 Bytes
712c393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import json
import os
from pathlib import Path
from typing import Literal

from dotenv import load_dotenv
from langchain_groq import ChatGroq
from langsmith import traceable
from pydantic import BaseModel, Field

ROOT_DIR = Path(__file__).resolve().parents[2]
PROMPT_FILE = ROOT_DIR / "prompts" / "query_classification.json"
ENV_FILE = ROOT_DIR / ".env"

load_dotenv(ENV_FILE)


class QueryClassification(BaseModel):
    category: Literal["fact", "detailed", "general"] = Field(
        description="The query category used to select the retrieval strategy."
    )
    confidence: float = Field(
        ge=0.0,
        le=1.0,
        description="Confidence score for the classification."
    )


class QueryCategorization:
    def __init__(

        self,

        prompt_file: Path = PROMPT_FILE,

        model_name: str | None = None,

    ) -> None:
        model_name = model_name or os.getenv("GROQ_MODEL", os.getenv("LLM_MODEL", "qwen/qwen3.8-27b"))
        api_key = os.getenv("GROQ_API_KEY")
        if not api_key:
            raise EnvironmentError("GROQ_API_KEY is not set.")

        self.prompt_file = Path(prompt_file)

        if not self.prompt_file.exists():
            raise FileNotFoundError(
                f"Prompt file not found: {self.prompt_file}"
            )

        self.system_prompt = self._load_prompt()
        self.model_name = model_name

        self.model = ChatGroq(
            model=model_name,
            api_key=api_key,
            temperature=0,
            max_retries=2,
        ).with_structured_output(QueryClassification)

    def _load_prompt(self) -> str:
        try:
            data = json.loads(
                self.prompt_file.read_text(encoding="utf-8")
            )
        except json.JSONDecodeError as exc:
            raise ValueError(
                f"Invalid JSON in {self.prompt_file}: {exc}"
            ) from exc

        if isinstance(data, dict):
            if isinstance(data.get("system"), str):
                return data["system"]

            query_classifier = data.get("query_classifier")
            if isinstance(query_classifier, dict):
                system_prompt = query_classifier.get("system")
                if isinstance(system_prompt, str):
                    return system_prompt

        raise ValueError(
            "query_classification.json must contain either "
            "'system' or 'query_classifier.system'."
        )

    @traceable(

        name="query_categorization",

        run_type="chain",

        tags=["rag", "query-classification"]

    )
    def classify(self, query: str) -> QueryClassification:
        if not isinstance(query, str):
            raise TypeError("Query must be a string.")

        query = query.strip()

        if not query:
            raise ValueError("Query cannot be empty.")

        result = self.model.invoke(
            [
                ("system", self.system_prompt),
                ("human", query)
            ],
            config={
                "metadata": {
                    "component": "query_categorization",
                    "model": self.model_name
                },
                "tags": [
                    "rag",
                    "query-classification",
                    "groq",
                    "open-source"
                ]
            }
        )

        if not isinstance(result, QueryClassification):
            raise TypeError(
                f"Unexpected output type: {type(result).__name__}"
            )

        return result


_classifier = None


def get_classifier() -> QueryCategorization:
    global _classifier

    if _classifier is None:
        _classifier = QueryCategorization()

    return _classifier


def classify_query(query: str) -> QueryClassification:
    return get_classifier().classify(query)


if __name__ == "__main__":
    classifier = QueryCategorization()

    test_queries = [
        "What was the company's revenue in 2025?",
        "Explain the complete employee onboarding process.",
        "Tell me about the company's AI strategy."
    ]

    for query in test_queries:
        try:
            result = classifier.classify(query)

            print(f"Query: {query}")
            print(f"Category: {result.category}")
            print(f"Confidence: {result.confidence:.2f}")
            print("-" * 60)

        except Exception as exc:
            print(f"Error: {exc}")