CultriX commited on
Commit
b07e47b
·
verified ·
1 Parent(s): 102efa3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +271 -0
app.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import asyncio
4
+ import logging
5
+ import threading
6
+ import queue
7
+ import gradio as gr
8
+ import httpx
9
+ from typing import Generator, Any, Dict, List
10
+
11
+ # -------------------- Configuration --------------------
12
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
13
+
14
+ # -------------------- External Model Call --------------------
15
+ async def call_model(prompt: str, model: str = "gpt-4o-mini", api_key: str = None) -> str:
16
+ """
17
+ Sends a prompt to the OpenAI API endpoint using the specified model (overridden to gpt-4o-mini)
18
+ and returns the generated response.
19
+ """
20
+ # Use the provided API key or fall back to the environment variable
21
+ if api_key is None:
22
+ api_key = os.getenv("OPENAI_API_KEY")
23
+ url = "https://api.openai.com/v1/chat/completions"
24
+ headers = {
25
+ "Authorization": f"Bearer {api_key}",
26
+ "Content-Type": "application/json"
27
+ }
28
+ # Override the model value to always be "gpt-4o-mini"
29
+ payload = {
30
+ "model": "gpt-4o-mini",
31
+ "messages": [{"role": "user", "content": prompt}],
32
+ }
33
+ async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client:
34
+ response = await client.post(url, headers=headers, json=payload)
35
+ response.raise_for_status()
36
+ response_json = response.json()
37
+ return response_json["choices"][0]["message"]["content"]
38
+
39
+ # -------------------- Agent Classes --------------------
40
+ class PromptOptimizerAgent:
41
+ async def optimize_prompt(self, user_prompt: str, api_key: str) -> str:
42
+ """
43
+ Optimizes the user's initial prompt according to the following instructions:
44
+ >>> Given the user's initial prompt below the ### characters please enhance it.
45
+ 1. Start with clear, precise instructions placed at the beginning of the prompt.
46
+ 2. Include specific details about the desired context, outcome, length, format, and style.
47
+ 3. Provide examples of the desired output format, if possible.
48
+ 4. Use appropriate leading words or phrases to guide the desired output, especially if code generation is involved.
49
+ 5. Avoid any vague or imprecise language.
50
+ 6. Rather than only stating what not to do, provide guidance on what should be done instead.
51
+ Remember to ensure the revised prompt remains true to the user's original intent. <<<
52
+ ###User initial prompt below ###
53
+ """
54
+ system_prompt = (
55
+ "Given the user's initial prompt below the ### characters please enhance it. "
56
+ "1. Start with clear, precise instructions placed at the beginning of the prompt. "
57
+ "2. Include specific details about the desired context, outcome, length, format, and style. "
58
+ "3. Provide examples of the desired output format, if possible. "
59
+ "4. Use appropriate leading words or phrases to guide the desired output, especially if code generation is involved. "
60
+ "5. Avoid any vague or imprecise language. "
61
+ "6. Rather than only stating what not to do, provide guidance on what should be done instead. "
62
+ "Remember to ensure the revised prompt remains true to the user's original intent. "
63
+ "###User initial prompt ###"
64
+ )
65
+ full_prompt = f"{system_prompt}\n{user_prompt}\n<<<"
66
+ optimized = await call_model(full_prompt, api_key=api_key)
67
+ return optimized
68
+
69
+ class OrchestratorAgent:
70
+ def __init__(self, log_queue: queue.Queue) -> None:
71
+ self.log_queue = log_queue
72
+
73
+ async def generate_plan(self, task: str, api_key: str) -> str:
74
+ """
75
+ Generates a detailed, step-by-step plan for completing the given task.
76
+ """
77
+ prompt = (
78
+ f"You are an orchestrator agent. The user has provided the task: '{task}'.\n"
79
+ "Generate a detailed, step-by-step plan for completing this task by coordinating a coder agent, "
80
+ "a code reviewer agent, and a documentation agent. List the steps as bullet points."
81
+ )
82
+ plan = await call_model(prompt, api_key=api_key)
83
+ return plan
84
+
85
+ class CoderAgent:
86
+ async def generate_code(self, instructions: str, api_key: str) -> str:
87
+ """
88
+ Generates code based on the given instructions.
89
+ """
90
+ prompt = (
91
+ "You are a coder agent. Based on the following instructions, generate the requested code. "
92
+ "Only output the generated code, never any explanations or any other information besides the actual code!\n"
93
+ f"{instructions}\n"
94
+ )
95
+ code = await call_model(prompt, api_key=api_key)
96
+ return code
97
+
98
+ class CodeReviewerAgent:
99
+ async def review_code(self, code: str, task: str, api_key: str) -> str:
100
+ """
101
+ Reviews the provided code to check if it meets the task specifications.
102
+ NEVER generate any code yourself! Respond only with feedback or with 'APPROVE' if everything is correct.
103
+ """
104
+ prompt = (
105
+ "You are a code reviewing agent highly skilled in evaluating code quality. "
106
+ "Review the provided code and check if it meets the task specifications and properly addresses any provided feedback. "
107
+ "NEVER generate any code yourself! Respond only with feedback or with 'APPROVE' if everything is correct. "
108
+ "Do not mention 'APPROVE' before actually approving! Do not request documentation or user guides:\n"
109
+ f"Task: {task}\n"
110
+ f"Code:\n{code}\n\n"
111
+ )
112
+ review = await call_model(prompt, api_key=api_key)
113
+ return review
114
+
115
+ class DocumentationAgent:
116
+ async def generate_documentation(self, code: str, api_key: str) -> str:
117
+ """
118
+ Generates clear and concise documentation for the approved code,
119
+ including a brief and concise --help message.
120
+ """
121
+ prompt = (
122
+ "You are a documentation agent. Generate a brief, clear and concise documentation for the following approved code. "
123
+ "Keep it short and compact, focusing on the main elements, do not include unnecessary extras that limit readability. "
124
+ "Additionally, generate a brief and concise --help message for the code:\n"
125
+ f"{code}\n"
126
+ "Briefly explain what the code does and how it works. Make sure to be clear and concise, do not include unnecessary extras that limit readability."
127
+ )
128
+ documentation = await call_model(prompt, api_key=api_key)
129
+ return documentation
130
+
131
+ # -------------------- Multi-Agent Conversation --------------------
132
+ async def multi_agent_conversation(task_message: str, log_queue: queue.Queue, api_key: str) -> None:
133
+ """
134
+ Conducts a multi-agent conversation where each agent's response is generated via the external model API.
135
+ The conversation is logged to the provided queue.
136
+ """
137
+ conversation: List[Dict[str, str]] = [] # List to store each agent's message
138
+
139
+ # Step 0: Use Prompt Optimizer to enhance the user's initial prompt.
140
+ log_queue.put("[Prompt Optimizer]: Received initial task. Optimizing prompt...")
141
+ prompt_optimizer = PromptOptimizerAgent()
142
+ optimized_task = await prompt_optimizer.optimize_prompt(task_message, api_key=api_key)
143
+ conversation.append({"agent": "Prompt Optimizer", "message": f"Optimized Task:\n{optimized_task}"})
144
+ log_queue.put(f"[Prompt Optimizer]: Optimized task prompt:\n{optimized_task}")
145
+
146
+ # Step 1: Orchestrator generates a plan based on the optimized task.
147
+ log_queue.put("[Orchestrator]: Received optimized task. Generating plan...")
148
+ orchestrator = OrchestratorAgent(log_queue)
149
+ plan = await orchestrator.generate_plan(optimized_task, api_key=api_key)
150
+ conversation.append({"agent": "Orchestrator", "message": f"Plan:\n{plan}"})
151
+ log_queue.put(f"[Orchestrator]: Plan generated:\n{plan}")
152
+
153
+ # Step 2: Coder generates code based on the plan.
154
+ coder = CoderAgent()
155
+ coder_instructions = f"Implement the task as described in the following plan:\n{plan}"
156
+ log_queue.put("[Coder]: Received coding task from the Orchestrator.")
157
+ code = await coder.generate_code(coder_instructions, api_key=api_key)
158
+ conversation.append({"agent": "Coder", "message": f"Code:\n{code}"})
159
+ log_queue.put(f"[Coder]: Code generated:\n{code}")
160
+
161
+ # Step 3: Code Reviewer reviews the generated code.
162
+ reviewer = CodeReviewerAgent()
163
+ approval_keyword = "approve"
164
+ revision_iteration = 0
165
+ while True:
166
+ if revision_iteration == 0:
167
+ log_queue.put("[Code Reviewer]: Starting review of the generated code...")
168
+ else:
169
+ log_queue.put(f"[Code Reviewer]: Reviewing the revised code (Iteration {revision_iteration})...")
170
+
171
+ review = await reviewer.review_code(code, optimized_task, api_key=api_key)
172
+ conversation.append({"agent": "Code Reviewer", "message": f"Review (Iteration {revision_iteration}):\n{review}"})
173
+ log_queue.put(f"[Code Reviewer]: Review feedback (Iteration {revision_iteration}):\n{review}")
174
+
175
+ # Check if the code has been approved
176
+ if approval_keyword in review.lower():
177
+ log_queue.put("[Code Reviewer]: Code approved.")
178
+ break # Exit the loop if approved
179
+
180
+ # If not approved, increment the revision count.
181
+ revision_iteration += 1
182
+
183
+ # Kill-switch: After 5 generations without approval, shut down.
184
+ if revision_iteration >= 5:
185
+ log_queue.put("Unable to solve your task to full satisfaction :(")
186
+ sys.exit("Unable to solve your task to full satisfaction :(")
187
+
188
+ # If under the limit, instruct the coder to revise the code.
189
+ log_queue.put(f"[Orchestrator]: Code not approved. Instructing coder to revise the code (Iteration {revision_iteration}).")
190
+ update_instructions = f"Please revise the code according to the following feedback. Feedback: {review}"
191
+ revised_code = await coder.generate_code(update_instructions, api_key=api_key)
192
+ conversation.append({"agent": "Coder", "message": f"Revised Code (Iteration {revision_iteration}):\n{revised_code}"})
193
+ log_queue.put(f"[Coder]: Revised code submitted (Iteration {revision_iteration}):\n{revised_code}")
194
+ code = revised_code # Update the code for the next review iteration
195
+
196
+ # Step 4: Documentation Agent generates documentation for the approved code.
197
+ doc_agent = DocumentationAgent()
198
+ log_queue.put("[Documentation Agent]: Generating documentation for the approved code.")
199
+ documentation = await doc_agent.generate_documentation(code, api_key=api_key)
200
+ conversation.append({"agent": "Documentation Agent", "message": f"Documentation:\n{documentation}"})
201
+ log_queue.put(f"[Documentation Agent]: Documentation generated:\n{documentation}")
202
+
203
+ log_queue.put("Multi-agent conversation complete.")
204
+ log_queue.put(("result", conversation))
205
+
206
+ # -------------------- Process Generator for Streaming --------------------
207
+ def process_conversation_generator(task_message: str, api_key: str) -> Generator[str, None, None]:
208
+ """
209
+ Wraps the asynchronous multi-agent conversation and yields log messages as they are generated.
210
+ """
211
+ log_q: queue.Queue = queue.Queue()
212
+
213
+ def run_conversation() -> None:
214
+ asyncio.run(multi_agent_conversation(task_message, log_q, api_key))
215
+
216
+ thread = threading.Thread(target=run_conversation)
217
+ thread.start()
218
+
219
+ final_result = None
220
+ # Yield log messages as long as the thread is running or the queue is not empty.
221
+ while thread.is_alive() or not log_q.empty():
222
+ try:
223
+ msg = log_q.get(timeout=0.1)
224
+ if isinstance(msg, tuple) and msg[0] == "result":
225
+ final_result = msg[1]
226
+ yield "Final conversation complete."
227
+ else:
228
+ yield msg
229
+ except queue.Empty:
230
+ continue
231
+
232
+ thread.join()
233
+ if final_result:
234
+ # Format the final conversation log.
235
+ conv_text = "\n========== Multi-Agent Conversation ==========\n"
236
+ for entry in final_result:
237
+ conv_text += f"[{entry['agent']}]: {entry['message']}\n\n"
238
+ yield conv_text
239
+
240
+ # -------------------- Chat Function for Gradio --------------------
241
+ def multi_agent_chat(message: str, history: List[Any], openai_api_key: str = None) -> Generator[str, None, None]:
242
+ """
243
+ Chat function for Gradio.
244
+ The user's message is interpreted as the task description.
245
+ An optional OpenAI API key can be provided via the additional input; if not provided, the environment variable is used.
246
+ This function streams the multi-agent conversation log messages.
247
+ """
248
+ if not openai_api_key:
249
+ openai_api_key = os.getenv("OPENAI_API_KEY")
250
+ yield from process_conversation_generator(message, openai_api_key)
251
+
252
+ # -------------------- Launch the Chatbot --------------------
253
+ # Use Gradio's ChatInterface with an additional input field for the OpenAI API key.
254
+ iface = gr.ChatInterface(
255
+ fn=multi_agent_chat,
256
+ additional_inputs=[gr.Textbox(label="OpenAI API Key (optional)", type="password", placeholder="Leave blank to use env variable")],
257
+ type="messages",
258
+ title="Actual Multi-Agent Conversation Chatbot",
259
+ description="""Collaborative workflow between Primary, Critic, and Documentation agents:
260
+ "1. Enter a task and observe as your prompt gets magically improved by the Prompt-Enhancing agent before reaching the
261
+ 2. Orchestrator, who then devises a plan and enlists the assistance of a
262
+ 3. Coder agent that writes code which is iteratively improved upon thanks to the
263
+ 4. Code Reviewer who finally decides if the code should be approved and get sent off to the
264
+ 5. Code Documentation Generator that will write documentation for your freshly generated code!"
265
+ "NOTE: The full conversation log will be displayed at the end, showing all the steps taken!"
266
+ "NOTE2: If the Coder is unable to satisfactorily complete the task after five attempts, the script will terminate to prevent endless iterations.
267
+ "NOTE3: You will have to input your OPENAI_API_KEY at the bottom of the page for this to work!"""
268
+ )
269
+
270
+ if __name__ == "__main__":
271
+ iface.launch()