File size: 7,055 Bytes
14fdc5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
266
267
from __future__ import annotations

import json
import re
from typing import Any


def chunk_text_for_stream(text: str, chunk_size: int = 36) -> list[str]:
    if not text:
        return []
    chunks: list[str] = []
    for idx in range(0, len(text), chunk_size):
        chunks.append(text[idx : idx + chunk_size])
    return chunks


def format_sse(event: str, data: dict[str, Any]) -> str:
    return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"


def extract_json_object(raw_text: str) -> dict[str, Any] | None:
    stripped = raw_text.strip()
    if not stripped:
        return None

    try:
        loaded = json.loads(stripped)
        if isinstance(loaded, dict):
            return loaded
    except json.JSONDecodeError:
        pass

    fenced = re.findall(r"```json\s*(\{.*?\})\s*```", stripped, flags=re.DOTALL)
    for block in fenced:
        try:
            loaded = json.loads(block)
            if isinstance(loaded, dict):
                return loaded
        except json.JSONDecodeError:
            continue

    brace_match = re.search(r"(\{.*\})", stripped, flags=re.DOTALL)
    if brace_match:
        candidate = brace_match.group(1)
        try:
            loaded = json.loads(candidate)
            if isinstance(loaded, dict):
                return loaded
        except json.JSONDecodeError:
            return None

    return None


# Intent phrases — any one of these anywhere in the message indicates the user wants
# the system to surface people. Designed to catch natural phrasing.
_CANDIDATE_SEARCH_INTENTS = (
    "give me",
    "show me",
    "find me",
    "fetch me",
    "get me",
    "send me",
    "bring me",
    "i need",
    "i want",
    "i'm looking",
    "im looking",
    "looking for",
    "looking to hire",
    "want to hire",
    "need to hire",
    "who has",
    "who knows",
    "who can",
    "anyone with",
    "any candidate",
    "any developer",
    "any engineer",
    "any profile",
    "search ",
    "find ",
    "fetch ",
    "rank ",
    "shortlist",
    "list ",
    "show ",
    "match ",
    "filter ",
    "top ",
    "best ",
    "candidate for",
    "candidates for",
    "candidate with",
    "candidates with",
    "candidate having",
    "candidates having",
    "candidate who",
    "candidates who",
    "engineer for",
    "engineers for",
    "developer for",
    "developers for",
)

_CANDIDATE_ROLE_WORDS = (
    "engineer",
    "engineers",
    "developer",
    "developers",
    "scientist",
    "scientists",
    "analyst",
    "analysts",
    "designer",
    "designers",
    "manager",
    "managers",
    "architect",
    "architects",
    "intern",
    "interns",
    "lead",
    "leads",
    "consultant",
    "consultants",
    "specialist",
    "specialists",
    "candidate",
    "candidates",
    "resume",
    "resumes",
    "profile",
    "profiles",
    "people",
    "talent",
    "experience",  # "candidate for AI experience"
    "expertise",
    "skill",
    "skills",
    "background",
)

_CANDIDATE_SEARCH_NEGATIVE_PHRASES = (
    "this pdf",
    "the pdf",
    "this resume",
    "the resume",
    "this file",
    "the file",
    "uploaded pdf",
    "uploaded resume",
    "uploaded file",
    "this candidate",
    "the candidate",
    "this person",
    "the person",
    "this applicant",
    "the applicant",
    "this profile",
    "the profile",
    "their resume",
    "their profile",
    "their experience",
    "his resume",
    "her resume",
    "his profile",
    "her profile",
    "about him",
    "about her",
)


def looks_like_candidate_search(query: str) -> bool:
    """Heuristic: does the user's message ask the system to find people?



    Accepts natural phrasing — "give me candidate for AI experience", "I need a

    frontend developer", "anyone with NLP background", "looking for top python

    engineers in Bangalore", etc. Requires both an *intent phrase* (give me / show

    me / find / I need / looking for / etc.) and a *role/skill noun* (engineer /

    developer / candidate / experience / skill / etc.).



    Filters out chat actions and messages about a specific uploaded resume so we

    don't hijack other flows.

    """
    lowered = (query or "").strip().lower()
    if not lowered:
        return False

    if lowered.startswith(("open_profile", "shortlist:", "details:", "publish_job_posting", "edit_job_posting")):
        return False

    if any(neg in lowered for neg in _CANDIDATE_SEARCH_NEGATIVE_PHRASES):
        return False

    has_intent = any(intent in lowered for intent in _CANDIDATE_SEARCH_INTENTS)
    has_role = any(role in lowered for role in _CANDIDATE_ROLE_WORDS)
    return has_intent and has_role


def parse_action_command(query: str) -> tuple[str, str] | None:
    """Detect chat messages that look like card action commands such as ``open_profile:<id>``."""
    if not query:
        return None

    cleaned = query.strip()
    if ":" not in cleaned:
        return None

    head, _, tail = cleaned.partition(":")
    head = head.strip().lower()
    tail = tail.strip()
    if not tail or " " in head:
        return None

    if head in {"open_profile", "details", "shortlist", "publish_job_posting", "edit_job_posting"}:
        return head, tail
    return None


def looks_like_c1_response(text: str) -> bool:
    lowered = text.lower()
    has_content_root = "<content" in lowered and "</content>" in lowered
    if not has_content_root:
        return False
    return (
        "<custom_markdown" in lowered
        or "<custommarkdown" in lowered
        or "<artifact" in lowered
    )


def wrap_text_as_c1(text: str) -> str:
    """Wrap plain markdown in the schema-stable C1 envelope.



    Thesys's <C1Component> renders <custom_markdown> reliably without their

    fine-tuned model — anything richer requires the API. Wrapping any agent

    text in this envelope means every response renders through the SDK,

    giving uniform typography/styling.

    """
    text = (text or "").strip()
    if not text:
        return ""
    if looks_like_c1_response(text):
        return text
    safe = (
        text.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
    )
    return f"<content><custom_markdown>{safe}</custom_markdown></content>"


def build_c1_response(text: str, payload: dict[str, Any] | None = None) -> str:
    clean_text = (text or "").strip()
    if clean_text and looks_like_c1_response(clean_text):
        return clean_text

    if payload:
        payload_c1 = payload.get("c1_response") or payload.get("c1Response")
        if isinstance(payload_c1, str):
            payload_c1 = payload_c1.strip()
            if payload_c1 and looks_like_c1_response(payload_c1):
                return payload_c1

    return ""