-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
265 lines (222 loc) · 10.8 KB
/
Copy pathmain.py
File metadata and controls
265 lines (222 loc) · 10.8 KB
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import os
from typing import Optional
from openai import AzureOpenAI
# --- Pydantic v2 -----------------------------------
from pydantic import BaseModel, Field, ConfigDict
# =============================================================================
# Configuration
# =============================================================================
client = AzureOpenAI(
azure_endpoint="",
api_key="",
api_version="",
)
# =============================================================================
# Utilities
# =============================================================================
def load_file(path: str, default: str = "") -> str:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return default
# =============================================================================
# Pydantic Tool Definitions (extras forbidden + all fields required)
# =============================================================================
class ConsultDocumentExpert(BaseModel):
"""Call the Document Expert for detailed technical questions about the document."""
model_config = ConfigDict(extra="forbid") # additionalProperties:false
# Use Field(...) to force REQUIRED in JSON Schema
question: str = Field(
..., description=(
"The user's question for Document Expert. "
"Use 'MODE_SWITCH' for explicit requests to talk to Document Expert (e.g., 'I want to talk to expert', 'Switch to expert mode'). "
"Otherwise, pass the actual question that needs document expertise."
)
)
class ReturnToGeneral(BaseModel):
"""Return control to the General Assistant for non-document questions."""
model_config = ConfigDict(extra="forbid") # additionalProperties:false
question: str = Field(
..., description=(
"The user's question for General Assistant. "
"Use 'MODE_SWITCH' for explicit requests to talk to General (e.g., 'I want to talk to general', 'Switch back'). "
"Otherwise, pass the actual question that General should handle."
)
)
# =============================================================================
# Chatbot
# =============================================================================
class DocumentChatbot:
def __init__(self) -> None:
# Load prompts once
self.general_prompt = load_file(
"first_prompt.md",
default="You are a General Assistant. Answer clearly and route document-heavy questions to the Document Expert tool.",
)
self.document_prompt_template = load_file(
"document_expert_prompt.md",
default="You are the Document Expert. Use the following document to answer with technical depth:\n\n[The full document content will be inserted here]",
)
self.document_content = load_file(
"document.md",
default="[Document content not found. Answer based on whatever is available.]",
)
self.document_prompt = self.document_prompt_template.replace(
"[The full document content will be inserted here]",
self.document_content,
)
self.mode = "general" # or "document"
# Optional: print schemas once to verify required/additionalProperties
if os.getenv("DEBUG_SCHEMA") == "1":
import json
print("\n[DEBUG] ConsultDocumentExpert schema:")
print(json.dumps(ConsultDocumentExpert.model_json_schema(), indent=2))
print("\n[DEBUG] ReturnToGeneral schema:")
print(json.dumps(ReturnToGeneral.model_json_schema(), indent=2))
# --------------------------- General LLM ---------------------------------
def general_llm(self, user_question: str) -> str:
# Special greeting if we just switched to General mode
if user_question == "MODE_SWITCH":
return (
"Hello! I'm the General Assistant. "
"I can help with general questions about the system, getting started, "
"and I'll connect you with the Document Expert for technical details when needed. "
"How can I help you today?"
)
try:
response = client.chat.completions.create(
model="GPT-4.1",
messages=[
{"role": "system", "content": self.general_prompt},
{"role": "user", "content": user_question},
],
tools=[
{
"type": "function",
"function": {
"name": "consult_document_expert",
"description": (
"Call when: 1) User asks about document technical details, algorithms, formulas. "
"2) User explicitly requests to talk to Document Expert (use question='MODE_SWITCH'). "
"3) You don't know the answer and it might be in the document (pass the actual question)."
),
"parameters": ConsultDocumentExpert.model_json_schema(),
"strict": True,
},
}
],
tool_choice="auto",
parallel_tool_calls=False,
)
message = response.choices[0].message
# Tool call?
if getattr(message, "tool_calls", None):
tool_call = message.tool_calls[0]
try:
# Validate tool args with Pydantic
document_request = ConsultDocumentExpert.model_validate_json(
tool_call.function.arguments
)
if document_request.question == "MODE_SWITCH":
print(f"\n[ROUTING] → Document Expert: Mode switch requested")
else:
print(f"\n[ROUTING] → Document Expert: {document_request.question[:50]}...")
self.mode = "document"
return self.document_expert_llm(document_request.question)
except Exception as e:
print(f"\n[ERROR] Tool call validation failed: {e}")
return "Sorry, there was an issue processing your document request."
return message.content if message.content else ""
except Exception as e:
print(f"\n[ERROR] General LLM failed: {e}")
return "Sorry, I encountered an error processing your question."
# --------------------------- Document Expert LLM ------------------------------
def document_expert_llm(self, user_question: str) -> str:
# Special greeting if we just switched to Document mode
if user_question == "MODE_SWITCH":
return (
"Hello! I'm the Document Expert, specialized in the provided document. "
"I can help with technical specifications, algorithms, formulas, and implementation details. "
"What would you like to explore?"
)
try:
response = client.chat.completions.create(
model="GPT-4.1",
messages=[
{"role": "system", "content": self.document_prompt},
{"role": "user", "content": user_question},
],
tools=[
{
"type": "function",
"function": {
"name": "return_to_general",
"description": (
"Call when: 1) User asks non-document questions (pass the actual question). "
"2) User explicitly requests to talk to General (use question='MODE_SWITCH')."
),
"parameters": ReturnToGeneral.model_json_schema(),
"strict": True,
},
}
],
tool_choice="auto",
parallel_tool_calls=False,
)
message = response.choices[0].message
# Tool call?
if getattr(message, "tool_calls", None):
tool_call = message.tool_calls[0]
try:
return_request = ReturnToGeneral.model_validate_json(
tool_call.function.arguments
)
if return_request.question == "MODE_SWITCH":
print(f"\n[ROUTING] → General: Mode switch requested")
else:
print(f"\n[ROUTING] → General: {return_request.question[:50]}...")
self.mode = "general"
return self.general_llm(return_request.question)
except Exception as e:
print(f"\n[ERROR] Tool call validation failed: {e}")
return "Sorry, there was an issue routing your question."
return message.content if message.content else ""
except Exception as e:
print(f"\n[ERROR] Document Expert failed: {e}")
return "Sorry, I encountered an error processing your document question."
# --------------------------- Router --------------------------------------
def chat(self, user_question: str) -> str:
if self.mode == "general":
return self.general_llm(user_question)
return self.document_expert_llm(user_question)
# =============================================================================
# CLI
# =============================================================================
def main() -> None:
print("Document Chatbot - Pydantic Structured Outputs (Azure)")
print("Type 'exit' to quit, 'debug' to show internal state")
print("-" * 60)
bot = DocumentChatbot()
while True:
mode_label = "General" if bot.mode == "general" else "Document Expert"
try:
user_input = input(f"\n[{mode_label}] You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nGoodbye!")
break
if not user_input:
continue
if user_input.lower() in {"exit", "quit"}:
print("Goodbye!")
break
if user_input.lower() == "debug":
print(f"Current mode: {bot.mode}")
print(f"General prompt loaded: {'✓' if bot.general_prompt else '✗'}")
print(f"Document prompt template loaded: {'✓' if bot.document_prompt_template else '✗'}")
print(f"Document content loaded: {'✓' if bot.document_content else '✗'}")
continue
reply = bot.chat(user_input)
print(f"\nBot: {reply}")
if __name__ == "__main__":
main()