Skip to content

Commit 169c486

Browse files
authored
Fixes for RAG docs (#952)
1 parent fb180ec commit 169c486

1 file changed

Lines changed: 10 additions & 261 deletions

File tree

agent-framework/agents/rag.md

Lines changed: 10 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ For conversation/session patterns alongside retrieval, see [Conversations & Memo
2020
## Using TextSearchProvider
2121

2222
The `TextSearchProvider` class is an out-of-the-box implementation of a RAG context provider.
23+
It supports different modes of operation, e.g. doing a search for each agent run with chat history, or advertising function tools for doing searches.
2324

24-
It can easily be attached to a `ChatClientAgent` using the `AIContextProviders` option to provide RAG capabilities to the agent.
25+
It can easily be attached to a `ChatClientAgent` using the `AIContextProviders` option.
2526

2627
```csharp
2728
// Configure the options for the TextSearchProvider.
@@ -30,7 +31,7 @@ TextSearchProviderOptions textSearchOptions = new()
3031
SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
3132
};
3233

33-
// Create the AI agent with the TextSearchProvider as the AI context provider.
34+
// Create the AI agent with the TextSearchProvider.
3435
AIAgent agent = azureOpenAIClient
3536
.GetChatClient(deploymentName)
3637
.AsAIAgent(new ChatClientAgentOptions
@@ -68,12 +69,12 @@ static Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapter(stri
6869

6970
### TextSearchProvider Options
7071

71-
The `TextSearchProvider` can be customized via the `TextSearchProviderOptions` class. Here is an example of creating options to run the search prior to every model invocation and keep a short rolling window of conversation context.
72+
The `TextSearchProvider` can be customized via the `TextSearchProviderOptions` class. Here is an example of creating options to run the search prior to every model invocation and keep a short rolling window of chat history for searches.
7273

7374
```csharp
7475
TextSearchProviderOptions textSearchOptions = new()
7576
{
76-
// Run the search prior to every model invocation and keep a short rolling window of conversation context.
77+
// Run the search prior to every model invocation and keep a short rolling window of chat history for searches.
7778
SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
7879
RecentMessageMemoryLimit = 6,
7980
};
@@ -83,17 +84,17 @@ The `TextSearchProvider` class supports the following options via the `TextSearc
8384

8485
| Option | Type | Description | Default |
8586
|--------|------|-------------|---------|
86-
| SearchTime | `TextSearchProviderOptions.TextSearchBehavior` | Indicates when the search should be executed. There are two options, each time the agent is invoked, or on-demand via function calling. | `TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke` |
87+
| SearchTime | `TextSearchProviderOptions.TextSearchBehavior` | Indicates when the search should be executed. There are two options, each time the agent is run, or on-demand via function calling. | `TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke` |
8788
| FunctionToolName | `string` | The name of the exposed search tool when operating in on-demand mode. | "Search" |
8889
| FunctionToolDescription | `string` | The description of the exposed search tool when operating in on-demand mode. | "Allows searching for additional information to help answer the user question." |
89-
| ContextPrompt | `string` | The context prompt prefixed to results when operating in `BeforeAIInvoke` mode. | "## Additional Context\nConsider the following information from source documents when responding to the user:" |
90-
| CitationsPrompt | `string` | The instruction appended after results to request citations when operating in `BeforeAIInvoke` mode. | "Include citations to the source document with document name and link if document name and link is available." |
91-
| ContextFormatter | `Func<IList<TextSearchProvider.TextSearchResult>, string>` | Optional delegate to fully customize formatting of the result list when operating in `BeforeAIInvoke` mode. If provided, `ContextPrompt` and `CitationsPrompt` are ignored. | `null` |
90+
| ContextPrompt | `string` | The context prompt prefixed to results. | "## Additional Context\nConsider the following information from source documents when responding to the user:" |
91+
| CitationsPrompt | `string` | The instruction appended after results to request citations. | "Include citations to the source document with document name and link if document name and link is available." |
92+
| ContextFormatter | `Func<IList<TextSearchProvider.TextSearchResult>, string>` | Optional delegate to fully customize formatting of the result list. If provided, `ContextPrompt` and `CitationsPrompt` are ignored. | `null` |
9293
| RecentMessageMemoryLimit | `int` | The number of recent conversation messages (both user and assistant) to keep in memory and include when constructing the search input for `BeforeAIInvoke` searches. | `0` (disabled) |
9394
| RecentMessageRolesIncluded | `List<ChatRole>` | The list of `ChatRole` types to filter recent messages to when deciding which recent messages to include when constructing the search input. | `ChatRole.User` |
9495

9596
> [!TIP]
96-
> See the [.NET samples](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples) for complete runnable examples.
97+
> See the [.NET samples](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/02-agents/AgentWithRAG) for complete runnable examples.
9798
9899
::: zone-end
99100
::: zone pivot="programming-language-python"
@@ -287,258 +288,6 @@ agent = chat_client.as_agent(
287288

288289
This approach allows the agent to choose the most appropriate search strategy based on the user's query.
289290

290-
### Complete example
291-
292-
```python
293-
# Copyright (c) Microsoft. All rights reserved.
294-
295-
import asyncio
296-
from collections.abc import MutableSequence, Sequence
297-
from typing import Any
298-
299-
from agent_framework import Agent, BaseContextProvider, Context, Message, SupportsChatGetResponse
300-
from agent_framework.azure import AzureAIClient
301-
from azure.identity.aio import AzureCliCredential
302-
from pydantic import BaseModel
303-
304-
305-
class UserInfo(BaseModel):
306-
name: str | None = None
307-
age: int | None = None
308-
309-
310-
class UserInfoMemory(BaseContextProvider):
311-
def __init__(self, client: SupportsChatGetResponse, user_info: UserInfo | None = None, **kwargs: Any):
312-
"""Create the memory.
313-
314-
If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
315-
"""
316-
317-
self._chat_client = client
318-
if user_info:
319-
self.user_info = user_info
320-
elif kwargs:
321-
self.user_info = UserInfo.model_validate(kwargs)
322-
else:
323-
self.user_info = UserInfo()
324-
325-
async def invoked(
326-
self,
327-
request_messages: Message | Sequence[Message],
328-
response_messages: Message | Sequence[Message] | None = None,
329-
invoke_exception: Exception | None = None,
330-
**kwargs: Any,
331-
) -> None:
332-
"""Extract user information from messages after each agent call."""
333-
# Check if we need to extract user info from user messages
334-
user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role == "user"] # type: ignore
335-
336-
if (self.user_info.name is None or self.user_info.age is None) and user_messages:
337-
try:
338-
# Use the chat client to extract structured information
339-
result = await self._chat_client.get_response(
340-
messages=request_messages, # type: ignore
341-
instructions="Extract the user's name and age from the message if present. "
342-
"If not present return nulls.",
343-
options={"response_format": UserInfo},
344-
)
345-
346-
# Update user info with extracted data
347-
try:
348-
extracted = result.value
349-
if self.user_info.name is None and extracted.name:
350-
self.user_info.name = extracted.name
351-
if self.user_info.age is None and extracted.age:
352-
self.user_info.age = extracted.age
353-
except Exception:
354-
pass # Failed to extract, continue without updating
355-
356-
except Exception:
357-
pass # Failed to extract, continue without updating
358-
359-
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
360-
"""Provide user information context before each agent call."""
361-
instructions: list[str] = []
362-
363-
if self.user_info.name is None:
364-
instructions.append(
365-
"Ask the user for their name and politely decline to answer any questions until they provide it."
366-
)
367-
else:
368-
instructions.append(f"The user's name is {self.user_info.name}.")
369-
370-
if self.user_info.age is None:
371-
instructions.append(
372-
"Ask the user for their age and politely decline to answer any questions until they provide it."
373-
)
374-
else:
375-
instructions.append(f"The user's age is {self.user_info.age}.")
376-
377-
# Return context with additional instructions
378-
return Context(instructions=" ".join(instructions))
379-
380-
def serialize(self) -> str:
381-
"""Serialize the user info for thread persistence."""
382-
return self.user_info.model_dump_json()
383-
384-
385-
async def main():
386-
async with AzureCliCredential() as credential:
387-
client = AzureAIClient(credential=credential)
388-
389-
# Create the memory provider
390-
memory_provider = UserInfoMemory(client)
391-
392-
# Create the agent with memory
393-
async with Agent(
394-
client=client,
395-
instructions="You are a friendly assistant. Always address the user by their name.",
396-
context_providers=[memory_provider],
397-
) as agent:
398-
# Create a new thread for the conversation
399-
thread = agent.create_session()
400-
401-
print(await agent.run("Hello, what is the square root of 9?", session=thread))
402-
print(await agent.run("My name is Ruaidhrí", session=thread))
403-
print(await agent.run("I am 20 years old", session=thread))
404-
405-
# Access the memory component and inspect the memories
406-
user_info_memory = memory_provider
407-
if user_info_memory:
408-
print()
409-
print(f"MEMORY - User Name: {user_info_memory.user_info.name}") # type: ignore
410-
print(f"MEMORY - User Age: {user_info_memory.user_info.age}") # type: ignore
411-
412-
413-
if __name__ == "__main__":
414-
asyncio.run(main())
415-
```
416-
417-
```python
418-
# Copyright (c) Microsoft. All rights reserved.
419-
420-
import asyncio
421-
from collections.abc import MutableSequence, Sequence
422-
from typing import Any
423-
424-
from agent_framework import Agent, BaseContextProvider, Context, Message, SupportsChatGetResponse
425-
from agent_framework.azure import AzureAIClient
426-
from azure.identity.aio import AzureCliCredential
427-
from pydantic import BaseModel
428-
429-
430-
class UserInfo(BaseModel):
431-
name: str | None = None
432-
age: int | None = None
433-
434-
435-
class UserInfoMemory(BaseContextProvider):
436-
def __init__(self, client: SupportsChatGetResponse, user_info: UserInfo | None = None, **kwargs: Any):
437-
"""Create the memory.
438-
439-
If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
440-
"""
441-
442-
self._chat_client = client
443-
if user_info:
444-
self.user_info = user_info
445-
elif kwargs:
446-
self.user_info = UserInfo.model_validate(kwargs)
447-
else:
448-
self.user_info = UserInfo()
449-
450-
async def invoked(
451-
self,
452-
request_messages: Message | Sequence[Message],
453-
response_messages: Message | Sequence[Message] | None = None,
454-
invoke_exception: Exception | None = None,
455-
**kwargs: Any,
456-
) -> None:
457-
"""Extract user information from messages after each agent call."""
458-
# Check if we need to extract user info from user messages
459-
user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role == "user"] # type: ignore
460-
461-
if (self.user_info.name is None or self.user_info.age is None) and user_messages:
462-
try:
463-
# Use the chat client to extract structured information
464-
result = await self._chat_client.get_response(
465-
messages=request_messages, # type: ignore
466-
instructions="Extract the user's name and age from the message if present. "
467-
"If not present return nulls.",
468-
options={"response_format": UserInfo},
469-
)
470-
471-
# Update user info with extracted data
472-
try:
473-
extracted = result.value
474-
if self.user_info.name is None and extracted.name:
475-
self.user_info.name = extracted.name
476-
if self.user_info.age is None and extracted.age:
477-
self.user_info.age = extracted.age
478-
except Exception:
479-
pass # Failed to extract, continue without updating
480-
481-
except Exception:
482-
pass # Failed to extract, continue without updating
483-
484-
async def invoking(self, messages: Message | MutableSequence[Message], **kwargs: Any) -> Context:
485-
"""Provide user information context before each agent call."""
486-
instructions: list[str] = []
487-
488-
if self.user_info.name is None:
489-
instructions.append(
490-
"Ask the user for their name and politely decline to answer any questions until they provide it."
491-
)
492-
else:
493-
instructions.append(f"The user's name is {self.user_info.name}.")
494-
495-
if self.user_info.age is None:
496-
instructions.append(
497-
"Ask the user for their age and politely decline to answer any questions until they provide it."
498-
)
499-
else:
500-
instructions.append(f"The user's age is {self.user_info.age}.")
501-
502-
# Return context with additional instructions
503-
return Context(instructions=" ".join(instructions))
504-
505-
def serialize(self) -> str:
506-
"""Serialize the user info for thread persistence."""
507-
return self.user_info.model_dump_json()
508-
509-
510-
async def main():
511-
async with AzureCliCredential() as credential:
512-
client = AzureAIClient(credential=credential)
513-
514-
# Create the memory provider
515-
memory_provider = UserInfoMemory(client)
516-
517-
# Create the agent with memory
518-
async with Agent(
519-
client=client,
520-
instructions="You are a friendly assistant. Always address the user by their name.",
521-
context_providers=[memory_provider],
522-
) as agent:
523-
# Create a new thread for the conversation
524-
thread = agent.create_session()
525-
526-
print(await agent.run("Hello, what is the square root of 9?", session=thread))
527-
print(await agent.run("My name is Ruaidhrí", session=thread))
528-
print(await agent.run("I am 20 years old", session=thread))
529-
530-
# Access the memory component and inspect the memories
531-
user_info_memory = memory_provider
532-
if user_info_memory:
533-
print()
534-
print(f"MEMORY - User Name: {user_info_memory.user_info.name}") # type: ignore
535-
print(f"MEMORY - User Age: {user_info_memory.user_info.age}") # type: ignore
536-
537-
538-
if __name__ == "__main__":
539-
asyncio.run(main())
540-
```
541-
542291
### Supported VectorStore Connectors
543292

544293
This pattern works with any Semantic Kernel VectorStore connector, including:

0 commit comments

Comments
 (0)