Skip to content

Commit cc3bff0

Browse files
Merge branch 'main' into sentiment_analysis_tool
2 parents e34ca0a + a246af2 commit cc3bff0

20 files changed

Lines changed: 1189 additions & 181 deletions

File tree

README.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,11 @@ A minimal Reason-and-Act (ReAct) agent for knowledge retrieval, implemented with
3838
- **[1.1 ReAct Agent for RAG](src/1_basics/1_react_rag/README.md)**
3939
Basic ReAct agent for step-by-step retrieval and answer generation.
4040

41-
4241
## Getting Started
4342

44-
Set your API keys in `.env`. Use `.env.example` as a template.
45-
46-
```bash
47-
cp -v .env.example .env
48-
```
43+
If you successfully created a workspace in Coder, you should already have a `.env` file in the repo.
4944

50-
Run integration tests to validate that your API keys are set up correctly.
45+
In that case you can verify that the API keys work by running integration tests with the following command:
5146

5247
```bash
5348
uv run --env-file .env pytest -sv tests/tool_tests/test_integration.py
@@ -91,7 +86,6 @@ As noted above, these are unnecessarily verbose for real applications.
9186
# uv run --env-file .env gradio src/1_basics/1_react_rag/app.py
9287
```
9388

94-
9589
### 2. Frameworks
9690

9791
Reason-and-Act Agent without the boilerplate- using the OpenAI Agent SDK.
@@ -105,15 +99,14 @@ Multi-agent examples, also via the OpenAI Agent SDK.
10599

106100
```bash
107101
uv run --env-file .env gradio src/2_frameworks/2_multi_agent/efficient.py
108-
# Verbose option- greater control over the agent flow, but less flexible.
102+
# Verbose option - greater control over the agent flow, but less flexible.
109103
# uv run --env-file .env gradio src/2_frameworks/2_multi_agent/verbose.py
110104
```
111105

112106
Python Code Interpreter demo- using the OpenAI Agent SDK, E2B for secure code sandbox, and LangFuse for observability. Refer to [src/2_frameworks/3_code_interpreter/README.md](src/2_frameworks/3_code_interpreter/README.md) for details.
113107

114108
MCP server integration example also via OpenAI Agents SDK with Gradio and Langfuse tracing. Refer to [src/2_frameworks/4_mcp/README.md](src/2_frameworks/4_mcp/README.md) for more details.
115109

116-
117110
### 3. Evals
118111

119112
Synthetic data.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dev = [
4444
"nbqa>=1.9.1",
4545
"pip-audit>=2.7.3",
4646
"pre-commit>=4.1.0",
47+
"pymupdf>=1.26.7",
4748
"pytest>=8.3.4",
4849
"pytest-asyncio>=1.2.0",
4950
"pytest-cov>=7.0.0",

src/2_frameworks/1_react_rag/langfuse_gradio.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import gradio as gr
1111
from dotenv import load_dotenv
1212
from gradio.components.chatbot import ChatMessage
13+
from langfuse import propagate_attributes
1314

1415
from src.prompts import REACT_INSTRUCTIONS
1516
from src.utils import (
@@ -51,9 +52,14 @@ async def _main(
5152
),
5253
)
5354

54-
with langfuse_client.start_as_current_span(name="Agents-SDK-Trace") as span:
55-
span.update(input=query)
56-
55+
with (
56+
langfuse_client.start_as_current_observation(
57+
name="Agents-SDK-Trace", as_type="agent", input=query
58+
) as obs,
59+
propagate_attributes(
60+
session_id=session.session_id # Propagate session_id to all child observations
61+
),
62+
):
5763
# Run the agent in streaming mode to get and display intermediate outputs
5864
result_stream = agents.Runner.run_streamed(
5965
main_agent, input=query, session=session
@@ -64,7 +70,7 @@ async def _main(
6470
if len(turn_messages) > 0:
6571
yield turn_messages
6672

67-
span.update(output=result_stream.final_output)
73+
obs.update(output=result_stream.final_output)
6874

6975
pretty_print(turn_messages)
7076
yield turn_messages
@@ -92,7 +98,7 @@ async def _main(
9298
[
9399
"At which university did the SVP Software Engineering"
94100
" at Apple (as of June 2025) earn their engineering degree?",
95-
]
101+
],
96102
],
97103
title="2.1: ReAct for Retrieval-Augmented Generation with OpenAI Agent SDK + LangFuse",
98104
)

src/2_frameworks/2_multi_agent/efficient.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import gradio as gr
1414
from dotenv import load_dotenv
1515
from gradio.components.chatbot import ChatMessage
16+
from langfuse import propagate_attributes
1617

1718
from src.prompts import REACT_INSTRUCTIONS
1819
from src.utils import (
@@ -39,20 +40,28 @@ async def _main(
3940
session = get_or_create_session(history, session_state)
4041

4142
# Use the main agent as the entry point- not the worker agent.
42-
with langfuse_client.start_as_current_span(name="Agents-SDK-Trace") as span:
43-
span.update(input=query)
44-
43+
with (
44+
langfuse_client.start_as_current_observation(
45+
name="Orchestrator-Worker", as_type="agent", input=query
46+
) as obs,
47+
propagate_attributes(
48+
session_id=session.session_id # Propagate session_id to all child observations
49+
),
50+
):
4551
# Run the agent in streaming mode to get and display intermediate outputs
4652
result_stream = agents.Runner.run_streamed(
47-
main_agent, input=query, session=session
53+
main_agent,
54+
input=query,
55+
session=session,
56+
max_turns=30, # Increase max turns to support more complex queries
4857
)
4958

5059
async for _item in result_stream.stream_events():
5160
turn_messages += oai_agent_stream_to_gradio_messages(_item)
5261
if len(turn_messages) > 0:
5362
yield turn_messages
5463

55-
span.update(output=result_stream.final_output)
64+
obs.update(output=result_stream.final_output)
5665

5766

5867
if __name__ == "__main__":
@@ -81,7 +90,11 @@ async def _main(
8190
instructions=(
8291
"You are a search agent. You receive a single search query as input. "
8392
"Use the search tool to perform a search, then produce a concise "
84-
"'search summary' of the key findings. Do NOT return raw search results."
93+
"'search summary' of the key findings. "
94+
"For every fact you include in the summary, ALWAYS include a citation "
95+
"both in-line and at the end of the summary as a numbered list. The "
96+
"citation at the end should include relevant metadata from the search "
97+
"results. Do NOT return raw search results. "
8598
),
8699
tools=[
87100
agents.function_tool(client_manager.knowledgebase.search_knowledgebase),
@@ -118,12 +131,13 @@ async def _main(
118131
**COMMON_GRADIO_CONFIG,
119132
examples=[
120133
[
121-
"At which university did the SVP Software Engineering"
122-
" at Apple (as of June 2025) earn their engineering degree?"
134+
"Write a structured report on the history of AI, covering: "
135+
"1) the start in the 50s, 2) the first AI winter, 3) the second AI winter, "
136+
"4) the modern AI boom, 5) the evolution of AI hardware, and "
137+
"6) the societal impacts of modern AI"
123138
],
124139
[
125-
"How does the annual growth in the 50th-percentile income "
126-
"in the US compare with that in Canada?",
140+
"Compare the box office performance of 'Oppenheimer' with the third Avatar movie"
127141
],
128142
],
129143
title="2.2.2: Multi-Agent Orchestrator-worker for Retrieval-Augmented Generation",

src/2_frameworks/2_multi_agent/efficient_multiple_kbs.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import gradio as gr
88
from dotenv import load_dotenv
99
from gradio.components.chatbot import ChatMessage
10+
from langfuse import propagate_attributes
1011

1112
from src.utils import (
1213
oai_agent_stream_to_gradio_messages,
@@ -36,9 +37,14 @@ async def _main(
3637
session = get_or_create_session(history, session_state)
3738

3839
# Use the main agent as the entry point- not the worker agent.
39-
with langfuse_client.start_as_current_span(name="Agents-SDK-Trace") as span:
40-
span.update(input=query)
41-
40+
with (
41+
langfuse_client.start_as_current_observation(
42+
name="Orchestrator-Worker", as_type="agent", input=query
43+
) as obs,
44+
propagate_attributes(
45+
session_id=session.session_id # Propagate session_id to all child observations
46+
),
47+
):
4248
# Run the agent in streaming mode to get and display intermediate outputs
4349
result_stream = agents.Runner.run_streamed(
4450
main_agent,
@@ -52,7 +58,7 @@ async def _main(
5258
if len(turn_messages) > 0:
5359
yield turn_messages
5460

55-
span.update(output=result_stream.final_output)
61+
obs.update(output=result_stream.final_output)
5662

5763

5864
if __name__ == "__main__":
@@ -173,26 +179,23 @@ async def _main(
173179
model=agents.OpenAIChatCompletionsModel(
174180
model=planner_model, openai_client=client_manager.openai_client
175181
),
182+
# NOTE: enabling parallel tool calls here can sometimes lead to issues with
183+
# with invalid arguments being passed to the search agent.
184+
model_settings=agents.ModelSettings(parallel_tool_calls=False),
176185
)
177186

178187
demo = gr.ChatInterface(
179188
_main,
180189
**COMMON_GRADIO_CONFIG,
181190
examples=[
182191
[
183-
"At which university did the SVP Software Engineering"
184-
" at Apple (as of June 2025) earn their engineering degree?"
185-
],
186-
[
187-
"How does the annual growth in the 50th-percentile income "
188-
"in the US compare with that in Canada?",
192+
"Write a structured report on the history of AI, covering: "
193+
"1) the start in the 50s, 2) the first AI winter, 3) the second AI winter, "
194+
"4) the modern AI boom, 5) the evolution of AI hardware, and "
195+
"6) the societal impacts of modern AI"
189196
],
190197
[
191-
"Provide a complete list of all countries that have a population "
192-
"over 100 million in 2026, that contain over 500 billion cubic meters "
193-
"of internal fresh water for the year 2021, and have a mortality rate "
194-
"less than the birth rate for the year 2021. The order of the list "
195-
"should be based on the largest population size in 2026."
198+
"Compare the box office performance of 'Oppenheimer' with the third Avatar movie"
196199
],
197200
],
198201
title="2.2.3: Multi-Agent Orchestrator-worker for Retrieval-Augmented Generation with Multiple Tools",

src/2_frameworks/2_multi_agent/fan_out.py

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,9 @@ async def process_document_pair(document_pair: DocumentPair) -> ConflictSummary
241241
242242
Returns None if exception is encountered.
243243
"""
244-
with langfuse_client.start_as_current_observation(name="Conflict- suggest") as span:
244+
with langfuse_client.start_as_current_observation(
245+
name="Conflict - suggest", as_type="agent"
246+
) as obs:
245247
try:
246248
result = await agents.Runner.run(
247249
worker_agent, input=document_pair.get_prompt()
@@ -251,7 +253,7 @@ async def process_document_pair(document_pair: DocumentPair) -> ConflictSummary
251253
print(e)
252254
return None
253255

254-
span.update(input=document_pair, output=output)
256+
obs.update(input=document_pair, output=output)
255257

256258
return output
257259

@@ -283,7 +285,9 @@ async def process_one_review(
283285
284286
Return None upon error.
285287
"""
286-
with langfuse_client.start_as_current_observation(name="Review proposal") as span:
288+
with langfuse_client.start_as_current_observation(
289+
name="Review proposal", as_type="agent"
290+
) as obs:
287291
try:
288292
result = await agents.Runner.run(
289293
conflict_review_agent, input=conflicted_document.model_dump_json()
@@ -293,7 +297,7 @@ async def process_one_review(
293297
print(e)
294298
return None
295299

296-
span.update(input=conflicted_document, output=output)
300+
obs.update(input=conflicted_document, output=output)
297301

298302
return output
299303

@@ -380,33 +384,43 @@ async def process_conflict_reviews(
380384
assert isinstance(dataset_dict, datasets.DatasetDict)
381385
documents = list(dataset_dict["train"])[: args.num_rows]
382386

383-
# Run O(N^2) agents on N documents to identify pairwise e.g., conflicts.
384-
document_pairs = build_document_pairs(documents) # type: ignore[arg-type]
385-
print(f"Built {len(document_pairs)} pair(s) from {len(documents)} document(s).")
386-
387-
with langfuse_client.start_as_current_span(name="Conflicts- Pairwise") as span:
388-
flagged_pairs = asyncio.get_event_loop().run_until_complete(
389-
process_fan_out(document_pairs)
390-
)
391-
span.update(
392-
input=args.source_dataset, output=f"{len(flagged_pairs)} pairs identified."
393-
)
394-
395-
# Collect conflicts related to each document.
396-
# from O(N^2) pairs to O(N) summarized per-document conflicts.
397-
conflicted_documents = group_conflicts(flagged_pairs)
387+
with langfuse_client.start_as_current_observation(
388+
name="Fan-Out", as_type="chain", input=args.source_dataset
389+
) as span:
390+
# Run O(N^2) agents on N documents to identify pairwise e.g., conflicts.
391+
document_pairs = build_document_pairs(documents) # type: ignore[arg-type]
392+
print(f"Built {len(document_pairs)} pair(s) from {len(documents)} document(s).")
393+
394+
with langfuse_client.start_as_current_observation(
395+
name="Conflicts - Pairwise", as_type="chain"
396+
) as obs:
397+
flagged_pairs = asyncio.get_event_loop().run_until_complete(
398+
process_fan_out(document_pairs)
399+
)
400+
obs.update(
401+
input=args.source_dataset,
402+
output=f"{len(flagged_pairs)} pairs identified.",
403+
)
398404

399-
# Review these O(N) per-document conflicts.
400-
with langfuse_client.start_as_current_span(name="Conflicts- Review") as span:
401-
conflict_reviews: list[ReviewedDocument] = (
402-
asyncio.get_event_loop().run_until_complete(
403-
process_conflict_reviews(conflicted_documents)
405+
# Collect conflicts related to each document.
406+
# from O(N^2) pairs to O(N) summarized per-document conflicts.
407+
conflicted_documents = group_conflicts(flagged_pairs)
408+
409+
# Review these O(N) per-document conflicts.
410+
with langfuse_client.start_as_current_observation(
411+
name="Conflicts - Review", as_type="chain"
412+
) as obs:
413+
conflict_reviews: list[ReviewedDocument] = (
414+
asyncio.get_event_loop().run_until_complete(
415+
process_conflict_reviews(conflicted_documents)
416+
)
404417
)
405-
)
406-
span.update(input=conflicted_documents, output=conflict_reviews)
418+
obs.update(input=conflicted_documents, output=conflict_reviews)
419+
420+
# Generate markdown output
421+
with open(args.output_report, "w") as output_file:
422+
reports = [_review.get_report() for _review in conflict_reviews]
423+
output_file.write("\n".join(reports))
424+
print(f"Writing report to {args.output_report}.")
407425

408-
# Generate markdown output
409-
with open(args.output_report, "w") as output_file:
410-
reports = [_review.get_report() for _review in conflict_reviews]
411-
output_file.write("\n".join(reports))
412-
print(f"Writing report to {args.output_report}.")
426+
span.update(output="Wrote report to " + args.output_report)

0 commit comments

Comments
 (0)