kumar4372 commited on
Commit
2d2c435
·
1 Parent(s): 4d634a3

initial commit

Browse files
.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
- GradioUI(agent).launch()
 
 
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]