agentic_llm 0.1.1 copy "agentic_llm: ^0.1.1" to clipboard
agentic_llm: ^0.1.1 copied to clipboard

Provider-independent chat and embedding models with streaming, tool calling and structured output. Adapters for OpenAI-compatible APIs, Anthropic and Gemini.

example/agentic_llm_example.dart

// Demonstrates the model layer: a provider-neutral request, a middleware stack,
// streaming, tool calling and cost accounting.
//
// Run it with:
//
//     dart run example/agentic_llm_example.dart
//
// It runs offline against `FakeChatModel`. To point it at a real provider,
// export a key first — nothing else changes, which is the point:
//
//     export OPENAI_API_KEY=sk-...
//     dart run example/agentic_llm_example.dart
import 'dart:io';

import 'package:agentic_core/agentic_core.dart';
import 'package:agentic_llm/agentic_llm.dart';
import 'package:agentic_llm/testing.dart';
import 'package:agentic_tools/agentic_tools.dart';

Future<void> main() async {
  // ---------------------------------------------------------------------------
  // 1. Pick a provider. Every branch produces a `ChatModel`, and nothing below
  //    this point knows or cares which one it got.
  // ---------------------------------------------------------------------------
  final apiKey = Platform.environment['OPENAI_API_KEY'];
  final anthropicKey = Platform.environment['ANTHROPIC_API_KEY'];

  final ChatModel provider;
  if (apiKey != null) {
    provider = OpenAiCompatibleChatModel.openAi(
      apiKey: apiKey,
      model: 'gpt-4o-mini',
      pricing: const ModelPricing(inputPerMillion: 0.15, outputPerMillion: 0.6),
    );
  } else if (anthropicKey != null) {
    provider = AnthropicChatModel(apiKey: anthropicKey);
  } else {
    print('No API key found — running against a scripted fake model.\n');
    provider = _scriptedModel();
  }

  // ---------------------------------------------------------------------------
  // 2. Wrap it. Each decorator is an ordinary class; the order is visible here
  //    rather than hidden in a pipeline configuration.
  // ---------------------------------------------------------------------------
  final model = ObservableChatModel(
    RetryingChatModel(
      CachingChatModel(
        FallbackChatModel(<ChatModel>[provider]),
        cache: InMemoryChatCache(),
      ),
      policy: RetryPolicy.interactive,
    ),
  );

  print('model        : ${model.info.qualifiedId}');
  print(
    'capabilities : ${model.info.capabilities.map((c) => c.name).join(', ')}',
  );

  // ---------------------------------------------------------------------------
  // 3. Observe the run. A UI would bind to these; here they just print.
  // ---------------------------------------------------------------------------
  final events = BroadcastEventBus();
  events.on<LlmEvent>().listen((event) {
    final detail = switch (event) {
      LlmRequestStarted(:final messageCount, :final toolCount) =>
        '$messageCount messages, $toolCount tools',
      LlmFirstTokenReceived(:final latency) =>
        'first token after ${latency.inMilliseconds}ms',
      LlmResponseCompleted(:final usage, :final cost, :final wasCached) =>
        '${usage.totalTokens} tokens'
            '${cost == null ? '' : ', \$${cost.toStringAsFixed(6)}'}'
            '${wasCached ? ' (cached)' : ''}',
      LlmRequestFailed(:final code) => 'failed: $code',
      _ => '',
    };
    print('  [${event.type}] $detail');
  });

  final context = AgenticContext.root(events: events, runId: 'example');

  // ---------------------------------------------------------------------------
  // 4. A plain question.
  // ---------------------------------------------------------------------------
  print('\n--- generate ---');
  final answer = await model.generate(
    ChatRequest.prompt(
      'What is the capital of France? Answer in one word.',
      system: 'You are concise.',
      temperature: 0,
    ),
    context: context,
  );
  print('answer       : ${answer.text}');
  print('finish       : ${answer.finishReason.name}');

  // The identical request is served from cache — note the zeroed usage.
  print('\n--- generate again (cache hit) ---');
  await model.generate(
    ChatRequest.prompt(
      'What is the capital of France? Answer in one word.',
      system: 'You are concise.',
      temperature: 0,
    ),
    context: context,
  );

  // ---------------------------------------------------------------------------
  // 5. Streaming. The same request, delivered incrementally.
  // ---------------------------------------------------------------------------
  print('\n--- stream ---');
  stdout.write('streamed     : ');
  final builder = ChatResponseBuilder(modelId: model.info.id);
  await for (final chunk in model.stream(
    ChatRequest.prompt('Name three Dart 3 features.', temperature: 0.7),
    context: context,
  )) {
    builder.add(chunk);
    if (chunk.textDelta case final delta?) stdout.write(delta);
  }
  stdout.writeln();
  print('chunks       : ${builder.chunkCount}');

  // ---------------------------------------------------------------------------
  // 6. Tool calling. The model asks; `agentic_tools` runs it; the loop closes.
  // ---------------------------------------------------------------------------
  print('\n--- tool calling ---');
  final registry = ToolRegistry()
    ..register(
      FunctionTool(
        name: 'get_weather',
        description: 'Returns the current weather for a city.',
        parameters: JsonSchema.object(
          properties: {
            'city': JsonSchema.string(description: 'City name'),
            'unit': JsonSchema.enumeration(['celsius', 'fahrenheit']),
          },
          required: {'city'},
        ),
        handler: (invocation) async => ToolResult.json(<String, Object?>{
          'city': invocation.require<String>('city'),
          'temperature': 18,
          'conditions': 'light rain',
        }),
      ),
    );

  final executor = ToolExecutor(tools: registry.all);
  var conversation = ChatRequest(
    messages: <Message>[Message.user('What is the weather in Paris?')],
    tools: registry.all,
    temperature: 0,
  );

  // A minimal agent loop. `agentic_agents` will own this; it is four lines
  // because the layers underneath already did the hard parts.
  for (var turn = 0; turn < 3; turn++) {
    final response = await model.generate(conversation, context: context);
    if (!response.hasToolCalls) {
      print('answer       : ${response.text}');
      break;
    }
    print('tool calls   : ${response.toolCalls.map((c) => c.name).join(', ')}');
    final results = await executor.executeAllAsMessages(
      response.toolCalls,
      context: context,
    );
    for (final result in results) {
      print('tool result  : ${result.toolResults.single.content}');
    }
    conversation = conversation.withMessages(<Message>[
      response.message,
      ...results,
    ]);
  }

  await events.dispose();
  await model.dispose();
  await registry.dispose();
}

/// A scripted model so the example runs with no network and no key.
ChatModel _scriptedModel() => FakeChatModel(
  info: ModelInfo(
    id: 'scripted',
    provider: 'example',
    capabilities: ModelCapabilities.frontier,
    pricing: const ModelPricing(inputPerMillion: 0.15, outputPerMillion: 0.6),
  ),
  turns: <FakeTurn>[
    FakeTurn.answer(
      ChatResponse(
        message: Message.assistant('Paris'),
        modelId: 'scripted',
        usage: const TokenUsage(promptTokens: 24, completionTokens: 1),
      ),
    ),
    FakeTurn.answer(
      ChatResponse(
        message: Message.assistant('Records, patterns, and sealed classes.'),
        modelId: 'scripted',
        usage: const TokenUsage(promptTokens: 18, completionTokens: 9),
      ),
    ),
    FakeTurn.answer(
      ChatResponse(
        message: Message.assistant(
          '',
          toolCalls: <ToolCallPart>[
            ToolCallPart(
              id: 'call_1',
              name: 'get_weather',
              arguments: <String, Object?>{'city': 'Paris'},
            ),
          ],
        ),
        modelId: 'scripted',
        finishReason: FinishReason.toolCalls,
        usage: const TokenUsage(promptTokens: 60, completionTokens: 12),
      ),
    ),
    FakeTurn.answer(
      ChatResponse(
        message: Message.assistant('It is 18 °C with light rain in Paris.'),
        modelId: 'scripted',
        usage: const TokenUsage(promptTokens: 90, completionTokens: 11),
      ),
    ),
  ],
);
2
likes
160
points
198
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Provider-independent chat and embedding models with streaming, tool calling and structured output. Adapters for OpenAI-compatible APIs, Anthropic and Gemini.

Repository (GitHub)
View/report issues
Contributing

Topics

#ai #llm #openai #anthropic #agentic

License

MIT (license)

Dependencies

agentic_core, agentic_tools, http, meta

More

Packages that depend on agentic_llm