microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
d9939fdbc6ed6fed27c32c245ecf1663562e5b8a

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

python/ta/test/cmpsearch.py

207lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4import argparse
5import asyncio
6import builtins
7from dataclasses import dataclass
8import json
9
10import numpy as np
11import typechat
12
13from typeagent.aitools import auth
14from typeagent.aitools.embeddings import AsyncEmbeddingModel
15from typeagent.demo import ui # TODO: Move what we import to a more appropriate place
16from typeagent.knowpro.convknowledge import create_typechat_model
17from typeagent.knowpro.interfaces import IConversation
18from typeagent.knowpro.search_query_schema import SearchQuery
19from typeagent.knowpro import searchlang
20from typeagent.podcasts.podcast import Podcast
21
22
23@dataclass
24class Context:
25 conversation: IConversation
26 query_translator: typechat.TypeChatJsonTranslator[SearchQuery]
27 embedding_model: AsyncEmbeddingModel
28 options: None
29 interactive: bool
30
31
32def main():
33 # Parse arguments.
34
35 default_qafile = (
36 "../../../AISystems-Archive/data/knowpro/test/Episode_53_Answer_results.json"
37 )
38 default_podcast_file = "testdata/Episode_53_AdrianTchaikovsky_index"
39
40 explanation = "a list of objects with 'question' and 'answer' keys"
41 parser = argparse.ArgumentParser(description="Parse Q/A data file")
42 parser.add_argument(
43 "--qafile",
44 type=str,
45 default=default_qafile,
46 help=f"Path to the data file ({explanation})",
47 )
48 parser.add_argument(
49 "--podcast",
50 type=str,
51 default=default_podcast_file,
52 help="Path to the podcast index files (excluding the '_index.json' suffix)",
53 )
54 parser.add_argument(
55 "--skip",
56 type=int,
57 default=0,
58 help="Number of initial Q/A pairs to skip (for debugging purposes)",
59 )
60 parser.add_argument(
61 "--interactive",
62 "-i",
63 action="store_true",
64 default=False,
65 help="Run in interactive mode, waiting for user input before each question",
66 )
67 args = parser.parse_args()
68
69 # Read evaluation data.
70
71 with open(args.qafile, "r") as file:
72 data = json.load(file)
73 assert isinstance(data, list), "Expected a list of Q/A pairs"
74 assert len(data) > 0, "Expected non-empty Q/A data"
75 assert all(
76 isinstance(qa_pair, dict) and "question" in qa_pair and "answer" in qa_pair
77 for qa_pair in data
78 ), "Expected each Q/A pair to be a dict with 'question' and 'answer' keys"
79
80 # Read podcast data.
81
82 auth.load_dotenv()
83 conversation = Podcast.read_from_file(args.podcast)
84 assert conversation is not None, f"Failed to load podcast from {file!r}"
85
86 # Create translator.
87
88 model = create_typechat_model()
89 query_translator = ui.create_translator(model, SearchQuery)
90
91 # Create context.
92
93 context = Context(
94 conversation,
95 query_translator,
96 AsyncEmbeddingModel(),
97 options=None, # TODO: Set options if needed
98 interactive=args.interactive,
99 )
100
101 # Loop over eval data, skipping duplicate questions
102 # (Those differ in 'cmd' value, which we don't support yet.)
103
104 skip = args.skip
105 last_q = ""
106 counter = 0
107 for qa_pair in data:
108 counter += 1
109 question = qa_pair.get("question")
110 answer = qa_pair.get("answer")
111 if not (question and answer) or question == last_q:
112 continue
113 last_q = question
114 if skip > 0:
115 skip -= 1
116 continue
117
118 # Wait for user input before continuing.
119 if context.interactive:
120 try:
121 input("Press Enter to continue... ")
122 except (EOFError, KeyboardInterrupt):
123 print()
124 break
125
126 # Compare the given answer with the actual answer for the question.
127 actual_answer, score = asyncio.run(compare(context, qa_pair))
128 print("-" * 25, counter, "-" * 25)
129 if not context.interactive and score < 0.97:
130 print(f"Question: {question}")
131 print(f"Expected answer:\n{answer}")
132 print("-" * 20)
133 print(f"Actual answer:\n{actual_answer}")
134 print(f"Score: {score:.3f}")
135 else:
136 print(f"Score: {score:.3f} (question: {question})")
137
138
139async def compare(
140 context: Context, qa_pair: dict[str, str | None]
141) -> tuple[str | None, float]:
142 the_answer: str | None = None
143 score = 0.0
144
145 question = qa_pair.get("question")
146 answer = qa_pair.get("answer")
147 cmd = qa_pair.get("cmd")
148 if not (question and answer):
149 return None, score
150
151 if not context.interactive:
152 print = lambda *args, **kwds: None # Disable printing in non-interactive mode
153 else:
154 print = builtins.print
155
156 print()
157 print("=" * 40)
158 if cmd:
159 print(f"Command: {cmd}")
160 print(f"Question: {question}")
161 print(f"Answer: {answer}")
162 print("-" * 40)
163
164 result = await searchlang.search_conversation_with_language(
165 context.conversation,
166 context.query_translator,
167 question,
168 context.options,
169 )
170 print("-" * 40)
171 if not isinstance(result, typechat.Success):
172 print("Error:", result.message)
173 else:
174 all_answers, combined_answer = await ui.generate_answers(
175 result.value, context.conversation, question
176 )
177 print("-" * 40)
178 if combined_answer.type == "NoAnswer":
179 print("Failure:", combined_answer.whyNoAnswer)
180 print("All answers:")
181 if context.interactive:
182 ui.pretty_print(all_answers)
183 else:
184 assert combined_answer.answer is not None, "Expected an answer"
185 the_answer = combined_answer.answer
186 score = await equality_score(context, answer, the_answer)
187 print(the_answer)
188 print("Correctness score:", score)
189 print("=" * 40)
190
191 return the_answer, score
192
193
194async def equality_score(context: Context, a: str, b: str) -> float:
195 a = a.strip()
196 b = b.strip()
197 if a == b:
198 return 1.0
199 if a.lower() == b.lower():
200 return 0.999
201 embeddings = await context.embedding_model.get_embeddings([a, b])
202 assert embeddings.shape[0] == 2, "Expected two embeddings"
203 return np.dot(embeddings[0], embeddings[1])
204
205
206if __name__ == "__main__":
207 main()