File size: 7,030 Bytes
cb3451f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import pytest

from hackathon_advisor.dashboard_chat_contracts import (
    CHAT_TOOL_SPECS,
    chat_tool_schemas,
    heuristic_chat_call,
    parse_native_tool_call,
    resolve_chat_tool_call,
    strip_function_blocks,
)
from hackathon_advisor.tool_contracts import ToolContractError


def test_chat_tool_schemas_are_openai_style() -> None:
    schemas = chat_tool_schemas()

    assert len(schemas) == len(CHAT_TOOL_SPECS) == 9
    for schema in schemas:
        assert schema["type"] == "function"
        assert schema["function"]["parameters"]["type"] == "object"


def test_parse_native_tool_call_reads_params() -> None:
    call = parse_native_tool_call(
        '<function name="search_projects"><param name="query">voice agents</param></function>'
    )

    assert call.name == "search_projects"
    assert call.arguments == {"query": "voice agents"}


def test_parse_native_tool_call_handles_cdata_and_surrounding_prose() -> None:
    call = parse_native_tool_call(
        'Let me look that up.\n<function name="show_cluster">'
        "<param name=\"label\"><![CDATA[Voice / Chatbot & ASR]]></param></function> Done."
    )

    assert call.name == "show_cluster"
    assert call.arguments == {"label": "Voice / Chatbot & ASR"}


def test_parse_native_tool_call_without_params() -> None:
    call = parse_native_tool_call('<function name="list_quests"></function>')

    assert call.name == "list_quests"
    assert call.arguments == {}


def test_parse_native_tool_call_rejects_garbage() -> None:
    with pytest.raises(ToolContractError):
        parse_native_tool_call('<function name="x"><param name="q">unclosed</function>')
    with pytest.raises(ToolContractError):
        parse_native_tool_call("just prose, no call")
    with pytest.raises(ToolContractError):
        parse_native_tool_call('<function><param name="q">missing name</param></function>')


def test_resolve_marks_plain_prose_as_none() -> None:
    resolution = resolve_chat_tool_call("Hello! Ask me about the atlas.", fallback_query="hello")

    assert resolution.status == "none"
    assert resolution.call is None
    assert resolution.errors == ()


def test_resolve_accepts_valid_call() -> None:
    resolution = resolve_chat_tool_call(
        '<function name="show_quest"><param name="quest">Tiny Titan</param></function>',
        fallback_query="tiny titan",
    )

    assert resolution.status == "valid"
    assert resolution.call is not None
    assert resolution.call.name == "show_quest"


def test_resolve_defaults_unknown_tool_through_intent_router() -> None:
    resolution = resolve_chat_tool_call(
        '<function name="made_up_tool"></function>',
        fallback_query="who completed the most quests?",
    )

    assert resolution.status == "defaulted"
    assert resolution.call is not None
    assert resolution.call.name == "top_projects_by_quests"
    assert resolution.errors


def test_resolve_defaults_malformed_xml_to_search() -> None:
    resolution = resolve_chat_tool_call(
        '<function name="search_projects"><param name="query">a < b</param></function>',
        fallback_query="projects about voice",
    )

    assert resolution.status == "defaulted"
    assert resolution.call is not None
    assert resolution.call.name == "search_projects"
    assert resolution.call.arguments["query"] == "projects about voice"


def test_resolve_rejects_missing_required_argument() -> None:
    resolution = resolve_chat_tool_call(
        '<function name="show_cluster"></function>',
        fallback_query="show me the voice cluster",
    )

    assert resolution.status == "defaulted"
    assert resolution.errors


def test_heuristic_chat_call_routes_intents() -> None:
    assert heuristic_chat_call("who completed the most quests").name == "top_projects_by_quests"
    assert heuristic_chat_call("what clusters exist").name == "list_clusters"
    assert heuristic_chat_call("list the quests please").name == "list_quests"
    assert heuristic_chat_call("what changed recently").name == "recent_activity"
    assert heuristic_chat_call("what is everyone building").name == "atlas_overview"
    assert heuristic_chat_call("knitting helpers").name == "search_projects"
    assert heuristic_chat_call("").name == "atlas_overview"


def test_data_intent_call_separates_detail_from_listing() -> None:
    from hackathon_advisor.dashboard_chat_contracts import data_intent_call

    detail = data_intent_call("what is in the Dream / Oracle cluster?")
    assert detail is not None and detail.name == "show_cluster"
    assert "Dream / Oracle" in detail.arguments["label"]

    quest_detail = data_intent_call("tell me about the Tiny Titan quest")
    assert quest_detail is not None and quest_detail.name == "show_quest"

    assert data_intent_call("what clusters exist").name == "list_clusters"
    assert data_intent_call("find projects about voice").name == "search_projects"
    assert data_intent_call("how many voice apps").name == "search_projects"
    assert data_intent_call("what is the coolest project?").name == "atlas_overview"
    assert data_intent_call("which project is most liked").name == "atlas_overview"
    assert data_intent_call("hello there") is None
    assert data_intent_call("thanks!") is None


def test_data_intent_call_routes_project_reading() -> None:
    from hackathon_advisor.dashboard_chat_contracts import data_intent_call

    readme = data_intent_call("read the readme of Jawbreaker")
    assert readme is not None and readme.name == "show_project"

    how = data_intent_call("how does Jawbreaker work?")
    assert how is not None and how.name == "show_project"

    about = data_intent_call("tell me about Jawbreaker")
    assert about is not None and about.name == "show_project"
    assert about.arguments["project"] == "tell me about Jawbreaker"

    # cluster/quest detail intents keep their own tools
    assert data_intent_call("tell me about the Tiny Titan quest").name == "show_quest"
    assert data_intent_call("what is in the Dream / Oracle cluster?").name == "show_cluster"


def test_smalltalk_intent_only_matches_greetings_and_followups() -> None:
    from hackathon_advisor.dashboard_chat_contracts import smalltalk_intent

    assert smalltalk_intent("hello!")
    assert smalltalk_intent("hey there")
    assert smalltalk_intent("thanks")
    assert smalltalk_intent("why")
    assert smalltalk_intent("are you sure?")
    assert smalltalk_intent("who are you")
    assert smalltalk_intent("")

    assert not smalltalk_intent("how many voice apps")
    assert not smalltalk_intent("what is the coolest project?")
    assert not smalltalk_intent("voice agents for elderly care")
    assert not smalltalk_intent("knitting helpers")


def test_strip_function_blocks_removes_stray_calls() -> None:
    text = 'Here you go. <function name="x"><param name="q">v</param></function> The map shows it.'

    assert strip_function_blocks(text) == "Here you go.  The map shows it.".strip()
    assert strip_function_blocks("plain prose") == "plain prose"