Spaces:
Runtime error
Runtime error
initial commit
Browse files- .gitignore +66 -0
- app.py +65 -2
- prompts.yaml +118 -0
- requirements.txt +1 -0
- tests/run_nlu_test.py +31 -0
- tools/nlu_tool.py +69 -0
- tools/requests_store.py +34 -0
- tools/scheduler.py +40 -0
.gitignore
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# Virtual environments
|
| 7 |
+
.venv/
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
env/
|
| 11 |
+
|
| 12 |
+
# Distribution / packaging
|
| 13 |
+
build/
|
| 14 |
+
dist/
|
| 15 |
+
*.egg-info/
|
| 16 |
+
.eggs/
|
| 17 |
+
|
| 18 |
+
# Installer logs
|
| 19 |
+
pip-log.txt
|
| 20 |
+
pip-delete-this-directory.txt
|
| 21 |
+
|
| 22 |
+
# Unit test / coverage
|
| 23 |
+
.pytest_cache/
|
| 24 |
+
.coverage
|
| 25 |
+
coverage.xml
|
| 26 |
+
|
| 27 |
+
# IDEs and editors
|
| 28 |
+
.vscode/
|
| 29 |
+
.idea/
|
| 30 |
+
*.sublime-workspace
|
| 31 |
+
*.sublime-project
|
| 32 |
+
|
| 33 |
+
# Mac
|
| 34 |
+
.DS_Store
|
| 35 |
+
|
| 36 |
+
# Logs and local data
|
| 37 |
+
logs/
|
| 38 |
+
*.log
|
| 39 |
+
/data/
|
| 40 |
+
|
| 41 |
+
# Requests store and operator notifications (sensitive/ephemeral)
|
| 42 |
+
data/requests.json
|
| 43 |
+
data/operator_notifications.log
|
| 44 |
+
|
| 45 |
+
# Python virtualenvs
|
| 46 |
+
.python-version
|
| 47 |
+
|
| 48 |
+
# Jupyter
|
| 49 |
+
.ipynb_checkpoints/
|
| 50 |
+
|
| 51 |
+
# Misc
|
| 52 |
+
*.sqlite3
|
| 53 |
+
*.env
|
| 54 |
+
.env
|
| 55 |
+
|
| 56 |
+
# Ignore compiled C extensions
|
| 57 |
+
*.so
|
| 58 |
+
|
| 59 |
+
# Node
|
| 60 |
+
node_modules/
|
| 61 |
+
|
| 62 |
+
# Terraform
|
| 63 |
+
.terraform/
|
| 64 |
+
|
| 65 |
+
# Local system files
|
| 66 |
+
Thumbs.db
|
app.py
CHANGED
|
@@ -3,7 +3,10 @@ import datetime
|
|
| 3 |
import requests
|
| 4 |
import pytz
|
| 5 |
import yaml
|
|
|
|
| 6 |
from tools.final_answer import FinalAnswerTool
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from Gradio_UI import GradioUI
|
| 9 |
|
|
@@ -18,6 +21,65 @@ def my_custom_tool(arg1:str, arg2:int)-> str: #it's import to specify the return
|
|
| 18 |
"""
|
| 19 |
return "What magic will you build ?"
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
@tool
|
| 22 |
def get_current_time_in_timezone(timezone: str) -> str:
|
| 23 |
"""A tool that fetches the current local time in a specified timezone.
|
|
@@ -55,7 +117,7 @@ with open("prompts.yaml", 'r') as stream:
|
|
| 55 |
|
| 56 |
agent = CodeAgent(
|
| 57 |
model=model,
|
| 58 |
-
tools=[final_answer, get_current_time_in_timezone], ## add your tools here (don't remove final answer)
|
| 59 |
max_steps=6,
|
| 60 |
verbosity_level=1,
|
| 61 |
grammar=None,
|
|
@@ -66,4 +128,5 @@ agent = CodeAgent(
|
|
| 66 |
)
|
| 67 |
|
| 68 |
|
| 69 |
-
|
|
|
|
|
|
| 3 |
import requests
|
| 4 |
import pytz
|
| 5 |
import yaml
|
| 6 |
+
import os
|
| 7 |
from tools.final_answer import FinalAnswerTool
|
| 8 |
+
from tools import nlu_tool, scheduler, requests_store
|
| 9 |
+
from smolagents import tool as tool_decorator
|
| 10 |
|
| 11 |
from Gradio_UI import GradioUI
|
| 12 |
|
|
|
|
| 21 |
"""
|
| 22 |
return "What magic will you build ?"
|
| 23 |
|
| 24 |
+
|
| 25 |
+
@tool_decorator
|
| 26 |
+
def nlu(text: str) -> dict:
|
| 27 |
+
"""Run NLU (intent + slots) on user text.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
text: The user's message to analyze.
|
| 31 |
+
"""
|
| 32 |
+
return nlu_tool.extract_intent_and_slots(text)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@tool_decorator
|
| 36 |
+
def propose_slots(preferred_windows: dict = None) -> list:
|
| 37 |
+
"""Return up to 3 candidate operator slots based on preferred windows.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
preferred_windows: Optional list of dicts with 'start' and 'end' strings
|
| 41 |
+
(natural language or ISO). Example: [{'start': 'tomorrow 09:00', 'end': 'tomorrow 12:00'}].
|
| 42 |
+
"""
|
| 43 |
+
cust_windows = []
|
| 44 |
+
if preferred_windows:
|
| 45 |
+
for w in preferred_windows:
|
| 46 |
+
try:
|
| 47 |
+
# use dateparser to flexibly parse the start/end and normalize to Asia/Tokyo
|
| 48 |
+
import dateparser
|
| 49 |
+
settings = {'TIMEZONE': 'Asia/Tokyo', 'RETURN_AS_TIMEZONE_AWARE': True}
|
| 50 |
+
s = dateparser.parse(w['start'], settings=settings)
|
| 51 |
+
e = dateparser.parse(w['end'], settings=settings)
|
| 52 |
+
if s and e:
|
| 53 |
+
cust_windows.append({'start': s, 'end': e})
|
| 54 |
+
except Exception:
|
| 55 |
+
continue
|
| 56 |
+
slots = scheduler.find_common_slots(cust_windows)
|
| 57 |
+
return slots
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@tool_decorator
|
| 61 |
+
def create_request(payload: dict) -> dict:
|
| 62 |
+
"""Persist a handoff request and return stored record.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
payload: dict containing the handoff data (customer, account, amount, date_by_when, etc.)
|
| 66 |
+
"""
|
| 67 |
+
return requests_store.create_request(payload)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@tool_decorator
|
| 71 |
+
def notify_operator(message: str) -> str:
|
| 72 |
+
"""Stub: notify operator (logs the message into data/operator_notifications.log)
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
message: The notification content to send to operator.
|
| 76 |
+
"""
|
| 77 |
+
os.makedirs('data', exist_ok=True)
|
| 78 |
+
path = 'data/operator_notifications.log'
|
| 79 |
+
with open(path, 'a') as f:
|
| 80 |
+
f.write(message + "\n---\n")
|
| 81 |
+
return "ok"
|
| 82 |
+
|
| 83 |
@tool
|
| 84 |
def get_current_time_in_timezone(timezone: str) -> str:
|
| 85 |
"""A tool that fetches the current local time in a specified timezone.
|
|
|
|
| 117 |
|
| 118 |
agent = CodeAgent(
|
| 119 |
model=model,
|
| 120 |
+
tools=[final_answer, get_current_time_in_timezone, nlu, propose_slots, create_request, notify_operator], ## add your tools here (don't remove final answer)
|
| 121 |
max_steps=6,
|
| 122 |
verbosity_level=1,
|
| 123 |
grammar=None,
|
|
|
|
| 128 |
)
|
| 129 |
|
| 130 |
|
| 131 |
+
if __name__ == '__main__':
|
| 132 |
+
GradioUI(agent).launch()
|
prompts.yaml
CHANGED
|
@@ -319,3 +319,121 @@
|
|
| 319 |
"report": |-
|
| 320 |
Here is the final answer from your managed agent '{{name}}':
|
| 321 |
{{final_answer}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
"report": |-
|
| 320 |
Here is the final answer from your managed agent '{{name}}':
|
| 321 |
{{final_answer}}
|
| 322 |
+
"collection_assistant": |-
|
| 323 |
+
description: "Conversation assistant helper for credit-card debt collection workflows. Use these intents and templates to detect customer intent, perform slot-filling, confirm commitments with the customer, and generate an operator handoff payload."
|
| 324 |
+
config:
|
| 325 |
+
default_confidence_threshold: 0.75
|
| 326 |
+
minimum_slot_confidence: 0.6
|
| 327 |
+
|
| 328 |
+
intents:
|
| 329 |
+
- name: detect_intent
|
| 330 |
+
description: "Classify utterances into one of: payment_commitment, request_human_operator, billing_question, update_contact, other"
|
| 331 |
+
sample_utterances:
|
| 332 |
+
- "I can pay X by DATE"
|
| 333 |
+
- "I need to talk to an operator"
|
| 334 |
+
- "My billing address changed"
|
| 335 |
+
- "I want to dispute a charge"
|
| 336 |
+
|
| 337 |
+
- name: payment_commitment
|
| 338 |
+
description: "Customer commits to a payment or partial payment by a certain date. The assistant should collect slots and confirm before handoff."
|
| 339 |
+
slots:
|
| 340 |
+
- id: customer_name
|
| 341 |
+
type: string
|
| 342 |
+
required: true
|
| 343 |
+
prompt: "Can I confirm your full name?"
|
| 344 |
+
- id: account_number
|
| 345 |
+
type: string
|
| 346 |
+
required: true
|
| 347 |
+
prompt: "Please provide the last 4 digits of your account number."
|
| 348 |
+
- id: amount
|
| 349 |
+
type: currency
|
| 350 |
+
currency: JPY
|
| 351 |
+
required: true
|
| 352 |
+
prompt: "How much do you plan to pay (approx) in Japanese yen (¥)?"
|
| 353 |
+
- id: date_by_when
|
| 354 |
+
type: date
|
| 355 |
+
required: true
|
| 356 |
+
prompt: "By which date can you make this payment?"
|
| 357 |
+
- id: contact_preference
|
| 358 |
+
type: enum
|
| 359 |
+
values: [sms, email, phone]
|
| 360 |
+
required: false
|
| 361 |
+
prompt: "How would you like us to contact you about this request? (sms/email/phone)"
|
| 362 |
+
confirmation_template: >-
|
| 363 |
+
Thank you {customer_name}. I have recorded that you'll pay {amount} (JPY) by {date_by_when} for account ending {account_number}.
|
| 364 |
+
I will send this to an operator for approval. You'll be notified when the status changes.
|
| 365 |
+
handoff_payload_template: |-
|
| 366 |
+
{
|
| 367 |
+
"type": "payment_commitment",
|
| 368 |
+
"customer_name": "{customer_name}",
|
| 369 |
+
"account_last4": "{account_number}",
|
| 370 |
+
"amount": "{amount}",
|
| 371 |
+
"date_by_when": "{date_by_when}",
|
| 372 |
+
"contact_preference": "{contact_preference}",
|
| 373 |
+
"nlu_confidence": {nlu_confidence}
|
| 374 |
+
}
|
| 375 |
+
operator_message_template: |-
|
| 376 |
+
New payment commitment needs approval.
|
| 377 |
+
Customer: {customer_name}
|
| 378 |
+
Account (last4): {account_number}
|
| 379 |
+
Amount: {amount}
|
| 380 |
+
Commit by: {date_by_when}
|
| 381 |
+
Contact pref: {contact_preference}
|
| 382 |
+
NLU confidence: {nlu_confidence}
|
| 383 |
+
Actions: [APPROVE, REJECT, REQUEST_CALL]
|
| 384 |
+
|
| 385 |
+
- name: request_human_operator
|
| 386 |
+
description: "Customer explicitly asks to speak with an operator. Assistant should check operator availability and propose common timeslots."
|
| 387 |
+
slots:
|
| 388 |
+
- id: customer_name
|
| 389 |
+
type: string
|
| 390 |
+
required: false
|
| 391 |
+
prompt: "May I have your name so I can connect you?"
|
| 392 |
+
- id: preferred_windows
|
| 393 |
+
type: string
|
| 394 |
+
required: false
|
| 395 |
+
prompt: "Do you have preferred times or days for a call?"
|
| 396 |
+
scheduling_instructions: |-
|
| 397 |
+
1) Query operator availability via calendar API (stub: tools.check_availability(customer_timezone, duration=15))
|
| 398 |
+
2) Find up to 3 candidate slots that overlap with customer's preferred windows (or next available slots if none provided).
|
| 399 |
+
3) Present candidate slots to customer for selection and confirm.
|
| 400 |
+
4) Once customer confirms, create handoff payload with proposed slot and notify operator.
|
| 401 |
+
propose_slots_template: >-
|
| 402 |
+
I can connect you with an operator. Here are some available times:
|
| 403 |
+
1) {slot1_local}
|
| 404 |
+
2) {slot2_local}
|
| 405 |
+
3) {slot3_local}
|
| 406 |
+
Which one works best for you?
|
| 407 |
+
operator_notification_template: |-
|
| 408 |
+
Customer requests a live operator call.
|
| 409 |
+
Customer: {customer_name}
|
| 410 |
+
Preferred windows: {preferred_windows}
|
| 411 |
+
Proposed slot: {confirmed_slot}
|
| 412 |
+
NLU confidence: {nlu_confidence}
|
| 413 |
+
|
| 414 |
+
- name: fallback
|
| 415 |
+
description: "Low-confidence or unrecognized requests. Ask clarifying question and avoid making commitments."
|
| 416 |
+
prompt: "I didn't fully understand that. Can you please rephrase or provide more details?"
|
| 417 |
+
|
| 418 |
+
behaviors:
|
| 419 |
+
- name: low_confidence_flow
|
| 420 |
+
when: "nlu_confidence < config.default_confidence_threshold"
|
| 421 |
+
actions:
|
| 422 |
+
- ask_clarifying_question: true
|
| 423 |
+
- avoid_handoff: true
|
| 424 |
+
|
| 425 |
+
- name: commit_then_handoff
|
| 426 |
+
when: "intent == payment_commitment and all required slots filled and nlu_confidence >= config.default_confidence_threshold"
|
| 427 |
+
actions:
|
| 428 |
+
- confirm_with_user: "confirmation_template"
|
| 429 |
+
- create_request_record: "handoff_payload_template"
|
| 430 |
+
- notify_user: "Your request has been created and an operator will review it. We'll notify you when the status changes."
|
| 431 |
+
- notify_operator: "operator_message_template"
|
| 432 |
+
|
| 433 |
+
examples: |
|
| 434 |
+
Example conversation - payment commitment:
|
| 435 |
+
Customer: "I can pay ¥30000 by next Friday"
|
| 436 |
+
Assistant: "Thanks — can I confirm your full name and last 4 of account?"
|
| 437 |
+
Customer: "Gautam Kumar, account 1234"
|
| 438 |
+
Assistant: "Great, I have recorded that you'll pay ¥30000 by 2025-10-10 for account ending 1234. I'll send this to an operator for approval and notify you when status changes."
|
| 439 |
+
|
requirements.txt
CHANGED
|
@@ -3,3 +3,4 @@ smolagents==1.13.0
|
|
| 3 |
requests
|
| 4 |
duckduckgo_search
|
| 5 |
pandas
|
|
|
|
|
|
| 3 |
requests
|
| 4 |
duckduckgo_search
|
| 5 |
pandas
|
| 6 |
+
dateparser
|
tests/run_nlu_test.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tools.nlu_tool import extract_intent_and_slots
|
| 2 |
+
from tools.scheduler import find_common_slots, get_operator_availability
|
| 3 |
+
import datetime
|
| 4 |
+
import pytz
|
| 5 |
+
from app import propose_slots
|
| 6 |
+
|
| 7 |
+
print('NLU test 1:')
|
| 8 |
+
r1 = extract_intent_and_slots('I can pay ¥30000 by next Friday')
|
| 9 |
+
print(r1)
|
| 10 |
+
print('parsed date_by_when:', r1.get('slots', {}).get('date_by_when'))
|
| 11 |
+
print('\nNLU test 2:')
|
| 12 |
+
print(extract_intent_and_slots('I need to talk to an operator'))
|
| 13 |
+
|
| 14 |
+
print('\nScheduler sample operator slots (first 3):')
|
| 15 |
+
ops = get_operator_availability()
|
| 16 |
+
for s in ops[:3]:
|
| 17 |
+
print(s.isoformat())
|
| 18 |
+
|
| 19 |
+
# Example customer window for find_common_slots (timezone-aware Asia/Tokyo)
|
| 20 |
+
TZ = pytz.timezone('Asia/Tokyo')
|
| 21 |
+
now = datetime.datetime.now(TZ)
|
| 22 |
+
start = TZ.localize(datetime.datetime(now.year, now.month, now.day, 9)) + datetime.timedelta(days=2)
|
| 23 |
+
end = TZ.localize(datetime.datetime(now.year, now.month, now.day, 12)) + datetime.timedelta(days=2)
|
| 24 |
+
slots = find_common_slots([{'start': start, 'end': end}])
|
| 25 |
+
print('\nCommon slots:')
|
| 26 |
+
for s in slots:
|
| 27 |
+
print(s)
|
| 28 |
+
|
| 29 |
+
print('\nPropose slots using preferred window ("tomorrow 09:00" to "tomorrow 12:00")')
|
| 30 |
+
prefs = [{'start': 'tomorrow 09:00', 'end': 'tomorrow 12:00'}]
|
| 31 |
+
print(propose_slots(prefs))
|
tools/nlu_tool.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from typing import Dict, Any, Tuple
|
| 3 |
+
import dateparser
|
| 4 |
+
import pytz
|
| 5 |
+
import datetime
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def parse_amount_and_currency(text: str) -> Tuple[str, float]:
|
| 9 |
+
"""Simple parser: looks for yen amounts like "¥30000" or "30000 yen" or numbers.
|
| 10 |
+
Returns tuple (currency, amount)
|
| 11 |
+
"""
|
| 12 |
+
# look for ¥ symbol
|
| 13 |
+
m = re.search(r"¥\s?([0-9,]+)", text)
|
| 14 |
+
if m:
|
| 15 |
+
amt = float(m.group(1).replace(',', ''))
|
| 16 |
+
return ("JPY", amt)
|
| 17 |
+
m = re.search(r"([0-9,]+)\s*(yen|JPY)\b", text, flags=re.I)
|
| 18 |
+
if m:
|
| 19 |
+
amt = float(m.group(1).replace(',', ''))
|
| 20 |
+
return ("JPY", amt)
|
| 21 |
+
# fallback: any number
|
| 22 |
+
m = re.search(r"([0-9,]+)", text)
|
| 23 |
+
if m:
|
| 24 |
+
return ("JPY", float(m.group(1).replace(',', '')))
|
| 25 |
+
return ("", 0.0)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def extract_intent_and_slots(text: str) -> Dict[str, Any]:
|
| 29 |
+
text_l = text.lower()
|
| 30 |
+
result = {
|
| 31 |
+
'intent': 'other',
|
| 32 |
+
'nlu_confidence': 0.5,
|
| 33 |
+
'slots': {}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# detect request for human
|
| 37 |
+
if any(kw in text_l for kw in ['operator', 'human', 'representative', 'staff', 'talk to']):
|
| 38 |
+
result['intent'] = 'request_human_operator'
|
| 39 |
+
result['nlu_confidence'] = 0.9
|
| 40 |
+
return result
|
| 41 |
+
|
| 42 |
+
# detect payment commitment
|
| 43 |
+
if any(kw in text_l for kw in ['pay', 'payment', 'i will pay', 'i can pay']):
|
| 44 |
+
result['intent'] = 'payment_commitment'
|
| 45 |
+
result['nlu_confidence'] = 0.85
|
| 46 |
+
# amount
|
| 47 |
+
currency, amount = parse_amount_and_currency(text)
|
| 48 |
+
if amount > 0:
|
| 49 |
+
result['slots']['amount'] = f"{int(amount)}"
|
| 50 |
+
# date: use dateparser with Japan timezone
|
| 51 |
+
settings = {'TIMEZONE': 'Asia/Tokyo', 'RETURN_AS_TIMEZONE_AWARE': True}
|
| 52 |
+
# try to parse phrases like 'by next Friday' or 'by 2025-10-10'
|
| 53 |
+
m = re.search(r"by\s+(.+)$", text, flags=re.I)
|
| 54 |
+
parsed = None
|
| 55 |
+
if m:
|
| 56 |
+
parsed = dateparser.parse(m.group(1).strip(), settings=settings)
|
| 57 |
+
if not parsed:
|
| 58 |
+
# try to parse full sentence for a date
|
| 59 |
+
parsed = dateparser.parse(text, settings=settings)
|
| 60 |
+
if parsed:
|
| 61 |
+
# normalize to Asia/Tokyo and isoformat
|
| 62 |
+
tz = pytz.timezone('Asia/Tokyo')
|
| 63 |
+
if parsed.tzinfo is None:
|
| 64 |
+
parsed = tz.localize(parsed)
|
| 65 |
+
else:
|
| 66 |
+
parsed = parsed.astimezone(tz)
|
| 67 |
+
result['slots']['date_by_when'] = parsed.isoformat()
|
| 68 |
+
|
| 69 |
+
return result
|
tools/requests_store.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
DATA_DIR = 'data'
|
| 6 |
+
REQUESTS_FILE = os.path.join(DATA_DIR, 'requests.json')
|
| 7 |
+
|
| 8 |
+
os.makedirs(DATA_DIR, exist_ok=True)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _load_all():
|
| 12 |
+
if not os.path.exists(REQUESTS_FILE):
|
| 13 |
+
return []
|
| 14 |
+
with open(REQUESTS_FILE, 'r') as f:
|
| 15 |
+
return json.load(f)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _save_all(items):
|
| 19 |
+
with open(REQUESTS_FILE, 'w') as f:
|
| 20 |
+
json.dump(items, f, indent=2, ensure_ascii=False)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def create_request(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
items = _load_all()
|
| 25 |
+
request_id = len(items) + 1
|
| 26 |
+
payload['id'] = request_id
|
| 27 |
+
payload['status'] = 'pending'
|
| 28 |
+
items.append(payload)
|
| 29 |
+
_save_all(items)
|
| 30 |
+
return payload
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def list_requests():
|
| 34 |
+
return _load_all()
|
tools/scheduler.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
from typing import List, Dict
|
| 3 |
+
import pytz
|
| 4 |
+
|
| 5 |
+
# Timezone-aware scheduler for Asia/Tokyo
|
| 6 |
+
TZ = pytz.timezone('Asia/Tokyo')
|
| 7 |
+
|
| 8 |
+
def _local_today():
|
| 9 |
+
return datetime.datetime.now(TZ)
|
| 10 |
+
|
| 11 |
+
def get_operator_availability(duration_minutes: int = 15) -> List[datetime.datetime]:
|
| 12 |
+
now = _local_today()
|
| 13 |
+
slots = []
|
| 14 |
+
for d in range(1,6):
|
| 15 |
+
day = now + datetime.timedelta(days=d)
|
| 16 |
+
for hour_min in [(10,0),(11,0),(14,0),(15,30)]:
|
| 17 |
+
slot = TZ.localize(datetime.datetime(day.year, day.month, day.day, hour_min[0], hour_min[1]))
|
| 18 |
+
slots.append(slot)
|
| 19 |
+
return slots
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def find_common_slots(customer_windows: List[Dict], duration_minutes: int =15, max_candidates: int =3):
|
| 23 |
+
"""Given customer's preferred windows (list of dicts with 'start' and 'end' datetimes),
|
| 24 |
+
return up to max_candidates slots that fit operator availability.
|
| 25 |
+
customer_windows example: [{'start': datetime, 'end': datetime}, ...]
|
| 26 |
+
Both operator slots and customer windows are assumed to be timezone-aware in Asia/Tokyo.
|
| 27 |
+
Returns list of ISO strings in Asia/Tokyo timezone.
|
| 28 |
+
"""
|
| 29 |
+
operator_slots = get_operator_availability(duration_minutes)
|
| 30 |
+
candidates = []
|
| 31 |
+
for s in operator_slots:
|
| 32 |
+
for w in customer_windows:
|
| 33 |
+
if w['start'] <= s <= w['end']:
|
| 34 |
+
candidates.append(s)
|
| 35 |
+
break
|
| 36 |
+
if len(candidates) >= max_candidates:
|
| 37 |
+
break
|
| 38 |
+
if not candidates:
|
| 39 |
+
candidates = operator_slots[:max_candidates]
|
| 40 |
+
return [dt.isoformat() for dt in candidates]
|