CultriX commited on
Commit
ffea40d
·
verified ·
1 Parent(s): 02d640a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +237 -147
app.py CHANGED
@@ -7,15 +7,50 @@ import queue
7
  import gradio as gr
8
  import httpx
9
  from typing import Generator, Any, Dict, List
10
- logger = logging.getLogger(__name__)
11
 
12
- class Agent(ABC):
13
- @abstractmethod
14
- async def generate_response(self, prompt: str, api_key: str) -> str:
15
- pass
16
 
17
- class PromptOptimizerAgent(Agent):
18
- async def generate_response(self, prompt: str, api_key: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  system_prompt = (
20
  "Given the user's initial prompt below the ### characters please enhance it. "
21
  "1. Start with clear, precise instructions placed at the beginning of the prompt. "
@@ -25,155 +60,210 @@ class PromptOptimizerAgent(Agent):
25
  "5. Avoid any vague or imprecise language. "
26
  "6. Rather than only stating what not to do, provide guidance on what should be done instead. "
27
  "Remember to ensure the revised prompt remains true to the user's original intent. "
28
- ###User initial prompt###
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  )
30
- return await call_openai(system_prompt, api_key)
31
-
32
- class OrchestratorAgent(Agent):
33
- async def generate_response(self, task_message: str, api_key: str) -> str:
34
- plan = f"You are an orchestrator agent. The user has provided the task: '{task_message}'. Generate a detailed, step-by-step plan for completing this task by coordinating a coder agent, a code reviewer agent, and a documentation agent. List the steps as bullet points."
35
- return await call_openai(plan, api_key)
36
-
37
- class CoderAgent(Agent):
38
- async def generate_response(self, instructions: str, api_key: str) -> str:
39
- prompt = f"Implement the task as described in the following plan:\n{instructions}"
40
- return await call_openai(prompt, api_key)
41
-
42
- class CodeReviewerAgent(Agent):
43
- async def generate_response(self, code: str, task: str, api_key: str) -> str:
44
- feedback = await call_openai(
45
- f"You are a code reviewer agent. Review the provided code: '{code}' and check if it meets the task specifications.",
46
- api_key=api_key
47
  )
48
- return feedback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- class DocumentationAgent(Agent):
51
- async def generate_response(self, code: str, api_key: str) -> str:
52
- prompt = f"You are a documentation agent. Generate a brief documentation for the code:\nCode:\n{code}"
53
- return await call_openai(prompt, api_key)
54
 
55
- async def process_conversation_generator(conversation: list, log_q: queue.Queue, api_key: str) -> Generator[str, None, None]:
56
- try:
57
- while True:
58
- if not conversation or not log_q.get(timeout=0.1):
59
- continue
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  msg = log_q.get(timeout=0.1)
62
  if isinstance(msg, tuple) and msg[0] == "result":
63
  final_result = msg[1]
64
- break
65
-
66
- yield msg
67
- except asyncio.CancelledError:
68
- pass
69
- finally:
70
- if log_q.empty():
71
- log_q.put("Final conversation complete.")
72
-
73
- async def multi_agent_conversation(
74
- task_message: str,
75
- log_q: queue.Queue,
76
- api_key: str,
77
- additional_inputs=None
78
- ) -> None:
79
- if additional_inputs is None:
80
- additional_inputs = [gr.Textbox(label="OpenAI API Key (optional)")]
81
-
82
- agents = [
83
- PromptOptimizerAgent(),
84
- OrchestratorAgent(),
85
- CoderAgent(),
86
- CodeReviewerAgent(),
87
- DocumentationAgent()
88
- ]
89
-
90
- log_queue = queue.Queue()
91
- run_conversation = None
92
-
93
- async def run_conversation_thread() -> None:
94
- nonlocal run_conversation
95
- try:
96
- if run_conversation is not None:
97
- await run_conversation
98
- except asyncio.CancelledError:
99
- pass
100
 
101
- thread = asyncio.to_thread(run_conversation_thread)
102
- thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- try:
105
- conversation = []
106
- log_q.put("[Prompt Optimizer]: Received initial task. Optimizing prompt...")
107
-
108
- # Step 0: Use Prompt Optimizer
109
- optimized_task = await agents[0].generate_response(task_message, api_key)
110
- conversation.append({"agent": "Prompt Optimizer", "message": f"Optimized Task:\n{optimized_task}"})
111
- log_q.put(f"[Prompt Optimizer]: Optimized Task:\n{optimized_task}")
112
-
113
- # Step 1: Generate Plan
114
- plan = await agents[1].generate_response(optimized_task, api_key)
115
- conversation.append({"agent": "Orchestrator", "message": f"Plan:\n{plan}"})
116
- log_q.put(f"[Orchestrator]: Plan generated:\n{plan}")
117
-
118
- # Step 2: Generate Code
119
- code = await agents[2].generate_response(plan, api_key)
120
- conversation.append({"agent": "Coder", "message": f"Code:\n{code}"})
121
- log_q.put(f"[Coder]: Code generated:\n{code}")
122
-
123
- # Step 3: Code Review
124
- code_review = None
125
- iteration = 0
126
- while True:
127
- if iteration >= 5:
128
- log_q.put("[Code Reviewer]: Code not approved after 5 iterations: terminating.")
129
- break
130
-
131
- code_review = await agents[3].generate_response(code, plan, api_key)
132
- revised_code = await agents[2].generate_response(
133
- f"Please revise the code according to the following feedback: {code_review}",
134
- api_key
135
- )
136
- code = revised_code
137
- iteration += 1
138
-
139
- if code == revised_code:
140
- break
141
-
142
- log_q.put(f"[Code Reviewer]: Feedback received:\n{code_review}")
143
- log_q.put(f"[Code Reviewer]: Revised code:\n{revised_code}")
144
-
145
- # Step 4: Documentation
146
- doc = await agents[4].generate_response(code, api_key)
147
- conversation.append({"agent": "Documentation Agent", "message": f"Documentation:\n{doc}"})
148
- log_q.put(f"[Documentation Agent]: Documentation generated:\n{doc}")
149
-
150
- except Exception as e:
151
- log_q.put(f"[All Agents]: An error occurred: {str(e)}")
152
- logger.error(f"Error in multi_agent_conversation: {str(e)}")
153
-
154
- finally:
155
- thread.join()
156
-
157
- async def multi_agent_conversation_wrapper(task_message: str, api_key: str) -> None:
158
- await multi_agent_conversation(
159
- task_message,
160
- log_q=queue.Queue(),
161
- api_key=api_key,
162
- additional_inputs=[gr.Textbox(label="OpenAI API Key (optional)") if api_key is None else None]
163
- )
164
 
165
  if __name__ == "__main__":
166
- iface = gr.ChatInterface(
167
- fn=multi_agent_conversation_wrapper,
168
- additional_inputs=[gr.Textbox(label="OpenAI API Key (optional)")],
169
- type="messages",
170
- title="Actual Multi-Agent Conversation Chatbot",
171
- description="""
172
- - Collaborative workflow between Prompt Enhancer, Orchestrator, Coder, Code-Reviewer and Documentation Agent agents.
173
- - Enter a task description to observe the iterative workflow between the agents.
174
- - NOTE: The kill-switch mechanism will terminate after five code rejection iterations to prevent endless loops.
175
- - NOTE3: You can input your OPENAI_API_KEY at the bottom of the page for this to work!
176
- """,
177
- )
178
-
179
- iface.launch()
 
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. "
 
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="""
260
+ - Collaborative workflow between Prompt Enhancer, Orchestrator, Coder, Code-Reviewer and Documentation Generator agents.
261
+ - Enter a task and observe as your prompt gets magically solved! :)
262
+ - NOTE: The full conversation log will be displayed at the end, showing all the steps taken!
263
+ - NOTE2: If the Coder is unable to satisfactorily complete the task after five attempts, the script will terminate to prevent endless iterations.
264
+ - NOTE3: You will have to input your OPENAI_API_KEY at the bottom of the page for this to work!
265
+ """
266
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  if __name__ == "__main__":
269
+ iface.launch()