thomascerniglia commited on
Commit
b0730a4
·
verified ·
1 Parent(s): cc800a6

Upload 7 files

Browse files
Files changed (7) hide show
  1. README.md +23 -13
  2. config.py +20 -0
  3. doc_utils.py +13 -0
  4. main.py +89 -0
  5. question_runner.py +65 -0
  6. requirements.txt +2 -0
  7. router_client.py +35 -0
README.md CHANGED
@@ -1,13 +1,23 @@
1
- ---
2
- title: AIClassicsQueryTool
3
- emoji: 🐠
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.49.1
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
+ # Classical Language Query Assistant
2
+
3
+ This app uses modern AI models to answer grammatical and syntactic questions about Latin and Greek passages. It's designed for use in research and pedagogy, especially in classical language instruction.
4
+
5
+ ## Features
6
+ - Supports Syntax and Morphology question sets
7
+ - Pulls questions live from shared Google Docs
8
+ - Uses Claude 3, GPT-3.5, and other fallback models via OpenRouter
9
+ - Automatically attributes which model answered each question
10
+
11
+ ## How to Run
12
+ 1. Clone or download this repo
13
+ 2. Install dependencies and launch the app:
14
+
15
+ ```bash
16
+ pip install -r requirements.txt
17
+ python main.py
18
+ ```
19
+
20
+ The app will open automatically in your browser with a public Gradio link.
21
+
22
+ ## Configuration
23
+ API keys, model priorities, and document URLs can be adjusted in `config.py`.
config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+
3
+ # === API SETTINGS ===
4
+ OPENROUTER_API_KEY = "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"
5
+ OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
6
+
7
+ # === GOOGLE DOCS INPUT ===
8
+ SYNTAX_DOC_URL = "https://docs.google.com/document/d/1Fx81TMaGh_s6vuOqG-6y9WVpVULriO6NrKovcrxqzmA/export?format=txt"
9
+ MORPHOLOGY_DOC_URL = "https://docs.google.com/document/d/1pk3CBDM2Vov2tGsfwxIlJp4h2QpPOEYuaqMpBSdBShw/export?format=txt"
10
+
11
+ # === MODEL PRIORITY ===
12
+ # Ordered from best to weakest. Will try top → bottom until one succeeds.
13
+ MODEL_PRIORITY = [
14
+ "anthropic/claude-3-haiku",
15
+ "openai/gpt-3.5-turbo",
16
+ "nousresearch/nous-hermes-2-mistral",
17
+ "meta-llama/llama-3-8b-instruct",
18
+ "mistralai/mistral-7b-instruct",
19
+ "gryphe/mythomax-l2-13b"
20
+ ]
doc_utils.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # doc_utils.py
2
+
3
+ import requests
4
+
5
+ def get_questions_from_doc(url):
6
+ try:
7
+ response = requests.get(url)
8
+ response.raise_for_status()
9
+ text = response.text
10
+ questions = [line.strip() for line in text.splitlines() if line.strip().endswith('?')]
11
+ return questions
12
+ except Exception as e:
13
+ return [f"Error loading questions: {e}"]
main.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+
3
+ import subprocess
4
+ import sys
5
+ import webbrowser
6
+ import time
7
+ import gradio as gr
8
+ from question_runner import run_tool
9
+ from config import MODEL_PRIORITY, SYNTAX_DOC_URL, MORPHOLOGY_DOC_URL
10
+ from doc_utils import get_questions_from_doc
11
+
12
+ # Auto-install required packages if missing
13
+ def install_missing_packages():
14
+ from importlib.metadata import distributions
15
+ required = {"gradio", "requests"}
16
+ installed = {dist.metadata['Name'].lower() for dist in distributions()}
17
+ missing = required - installed
18
+
19
+ if missing:
20
+ print(f"Installing missing packages: {missing}")
21
+ subprocess.check_call([sys.executable, "-m", "pip", "install", *missing])
22
+
23
+ install_missing_packages()
24
+
25
+ # Estimate runtime based on # of questions
26
+ def estimate_runtime(passage, doc_type):
27
+ if not passage.strip() or not doc_type:
28
+ return ""
29
+ doc_url = SYNTAX_DOC_URL if doc_type.lower() == "syntax" else MORPHOLOGY_DOC_URL
30
+ questions = get_questions_from_doc(doc_url)
31
+ if not questions or questions[0].startswith("Error"):
32
+ return "Unable to load questions."
33
+ est_seconds = round(len(questions) * 2.5, 1)
34
+ return f"Estimated generation time: ~{est_seconds} seconds"
35
+
36
+ def launch_app():
37
+ with gr.Blocks(theme="soft") as demo:
38
+ gr.Markdown("""
39
+ ## **Classical Language Query Assistant**
40
+ Submit a Latin or Greek passage and select the question type.
41
+ Answers are generated using a rotating chain of hosted AI models via OpenRouter.
42
+
43
+ - Models are attempted in descending priority, starting from the most accurate.
44
+ - The model that answers each question is recorded in the response.
45
+ - Model quota or errors may trigger automatic fallback to the next-best option.
46
+ """)
47
+
48
+ with gr.Row():
49
+ passage_input = gr.Textbox(label="Latin or Greek Passage", lines=4)
50
+ question_type = gr.Radio(["Syntax", "Morphology"], label="Question Type")
51
+
52
+ top_model = MODEL_PRIORITY[0]
53
+ full_model_list = "\n".join(f"- `{m}`" for m in MODEL_PRIORITY)
54
+ demo_model_info = gr.Markdown(
55
+ f"""
56
+ **Currently prioritized model:** `{top_model}`
57
+ **Model fallback chain (if needed):**
58
+ {full_model_list}
59
+ """)
60
+
61
+ with gr.Row():
62
+ output_text = gr.Textbox(label="Generated Answers", lines=25, interactive=False)
63
+ output_file = gr.File(label="Download Answers (.txt)", interactive=False)
64
+
65
+ estimated_time_box = gr.Textbox(label="Estimated Time", interactive=False)
66
+
67
+ # Trigger time estimate dynamically
68
+ passage_input.change(fn=estimate_runtime, inputs=[passage_input, question_type], outputs=estimated_time_box)
69
+ question_type.change(fn=estimate_runtime, inputs=[passage_input, question_type], outputs=estimated_time_box)
70
+
71
+ submit_button = gr.Button("Generate Answers")
72
+
73
+ submit_button.click(
74
+ fn=run_tool,
75
+ inputs=[passage_input, question_type],
76
+ outputs=[output_text, output_file, estimated_time_box]
77
+ )
78
+
79
+ # Launch app and open browser
80
+ _, _, share_url = demo.launch(share=True, prevent_thread_lock=True)
81
+ if share_url:
82
+ webbrowser.open(share_url)
83
+
84
+ # Keep app running
85
+ while True:
86
+ time.sleep(1)
87
+
88
+ if __name__ == "__main__":
89
+ launch_app()
question_runner.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # question_runner.py
2
+
3
+ import tempfile
4
+ from router_client import query_model
5
+ from doc_utils import get_questions_from_doc
6
+ from config import SYNTAX_DOC_URL, MORPHOLOGY_DOC_URL
7
+
8
+ def run_tool(passage, doc_type):
9
+ if not passage.strip():
10
+ return "Please enter a passage to analyze.", None, None
11
+ if not doc_type:
12
+ return "Please select either 'Syntax' or 'Morphology'.", None, None
13
+
14
+ try:
15
+ doc_url = SYNTAX_DOC_URL if doc_type.lower() == "syntax" else MORPHOLOGY_DOC_URL
16
+ questions = get_questions_from_doc(doc_url)
17
+
18
+ if not questions or questions[0].startswith("Error"):
19
+ return questions[0], None, None
20
+
21
+ est_seconds = round(len(questions) * 2.5, 1)
22
+ estimated_time_message = f"Estimated generation time: ~{est_seconds} seconds"
23
+
24
+ responses = []
25
+ for idx, question in enumerate(questions):
26
+ prompt = f"""You are a classical language expert.
27
+
28
+ Given the following Latin or Greek passage:
29
+
30
+ {passage}
31
+
32
+ Answer the following question:
33
+
34
+ {question}
35
+
36
+ Answer:"""
37
+
38
+ raw_response, model_used = query_model(prompt)
39
+
40
+ if not raw_response or not model_used:
41
+ formatted_block = f"""Question: {question.strip()}
42
+ Answer:
43
+ <No answer – all models failed or quota exceeded.>
44
+ ===
45
+ """
46
+ else:
47
+ answer = raw_response.split("Answer:")[-1].strip()
48
+ formatted_block = f"""Question: {question.strip()}
49
+ Answer:
50
+ {answer}
51
+ Model used: {model_used}
52
+ ===""" # Separator for logic tree parsing
53
+
54
+ responses.append(formatted_block)
55
+
56
+ result = "\n\n".join(responses)
57
+
58
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8") as f:
59
+ f.write(result)
60
+ file_path = f.name
61
+
62
+ return result, file_path, estimated_time_message
63
+
64
+ except Exception as e:
65
+ return f"An error occurred: {str(e)}", None, None
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0
router_client.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # router_client.py
2
+
3
+ import requests
4
+ from config import OPENROUTER_API_KEY, OPENROUTER_API_URL, MODEL_PRIORITY
5
+
6
+ def query_model(prompt):
7
+ headers = {
8
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
9
+ "Content-Type": "application/json"
10
+ }
11
+
12
+ for model in MODEL_PRIORITY:
13
+ try:
14
+ print(f"Trying model: {model}")
15
+ payload = {
16
+ "model": model,
17
+ "messages": [
18
+ {"role": "system", "content": "You are a classical language expert. Answer clearly and precisely."},
19
+ {"role": "user", "content": prompt}
20
+ ],
21
+ "temperature": 0.7,
22
+ "max_tokens": 1000
23
+ }
24
+
25
+ response = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=60)
26
+ response.raise_for_status()
27
+
28
+ content = response.json()["choices"][0]["message"]["content"]
29
+ return content.strip(), model
30
+
31
+ except requests.exceptions.RequestException as e:
32
+ print(f"Model {model} failed: {e}")
33
+ continue
34
+
35
+ return "All models failed or quota exceeded.", None